Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.62% covered (warning)
64.62%
42 / 65
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
GlobalRenameDenylist
64.62% covered (warning)
64.62%
42 / 65
50.00% covered (danger)
50.00%
2 / 4
34.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetchList
44.44% covered (danger)
44.44%
16 / 36
0.00% covered (danger)
0.00%
0 / 1
31.75
 checkUser
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
6.07
1<?php
2/**
3 * @section LICENSE
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 *
19 * @file
20 */
21
22namespace MediaWiki\Extension\CentralAuth\GlobalRename;
23
24use BadMethodCallException;
25use MediaWiki\Content\WikitextContent;
26use MediaWiki\Http\HttpRequestFactory;
27use MediaWiki\Page\WikiPageFactory;
28use MediaWiki\Title\Title;
29use Psr\Log\LoggerInterface;
30use StringUtils;
31
32/**
33 * Utility class to deal with global rename denylist.
34 *
35 * @author Martin Urbanec <martin.urbanec@wikimedia.cz>
36 * @copyright © 2020 Martin Urbanec
37 */
38class GlobalRenameDenylist {
39
40    /** @var string|Title|null Source of the denylist, url to fetch it from, or null */
41    private $file = null;
42
43    /** @var string[]|null Content of the denylist */
44    private $denylist = null;
45
46    private LoggerInterface $logger;
47    private HttpRequestFactory $httpRequestFactory;
48    private WikiPageFactory $wikiPageFactory;
49
50    /**
51     * @param LoggerInterface $logger
52     * @param HttpRequestFactory $httpRequestFactory
53     * @param WikiPageFactory $wikiPageFactory
54     * @param string|Title|null $denylistSource Page with denylist, url to fetch it from,
55     *   or null for no list ($wgGlobalRenameDenylist)
56     */
57    public function __construct(
58        LoggerInterface $logger,
59        HttpRequestFactory $httpRequestFactory,
60        WikiPageFactory $wikiPageFactory,
61        $denylistSource
62    ) {
63        $this->logger = $logger;
64        $this->httpRequestFactory = $httpRequestFactory;
65        $this->wikiPageFactory = $wikiPageFactory;
66        $this->file = $denylistSource;
67    }
68
69    /**
70     * Is global rename denylist enabled?
71     *
72     * @return bool
73     */
74    private function isEnabled(): bool {
75        return $this->file !== null;
76    }
77
78    /**
79     * Internal method for fetching denylist.
80     *
81     * Denylist is fetched and parsed into denylist. Denylist source is
82     * either an URL on the internet, or a wiki page.
83     * $url has to be already set.
84     */
85    private function fetchList() {
86        if ( $this->denylist !== null && count( $this->denylist ) !== 0 ) {
87            throw new BadMethodCallException(
88                'GlobalRenameDenylist::fetchList called on already fully initialized class'
89            );
90        }
91
92        if ( $this->file instanceof Title ) {
93            $this->logger->debug( 'GlobalRenameDenylist is fetching denylist from a wikipage' );
94            $wikipage = $this->wikiPageFactory->newFromTitle( $this->file );
95            $content = $wikipage->getContent();
96            if ( $content === null ) {
97                throw new BadMethodCallException(
98                    'GlobalRenameDenylist::fetchList was called with non-existent wikipage'
99                );
100            }
101            if ( !$content instanceof WikitextContent ) {
102                throw new BadMethodCallException(
103                    'Page used with GlobalRenameDenylist has invalid content model'
104                );
105            }
106            $text = $content->getText();
107        } else {
108            $this->logger->debug( 'GlobalRenameDenylist is fetching denylist from the internet' );
109            if ( $this->file === null ) {
110                $this->logger->info( 'GlobalRenameDenylist is not specified, not fetching anything' );
111                $this->denylist = [];
112                return;
113            }
114            $text = $this->httpRequestFactory->get( $this->file, [], __METHOD__ );
115            if ( $text === null ) {
116                $this->logger->error( 'GlobalRenameDenylist failed to fetch global rename denylist.' );
117                $this->denylist = [];
118                return;
119            }
120        }
121
122        $rows = explode( "\n", $text );
123        $this->denylist = [];
124        foreach ( $rows as $row ) {
125            $trimmedRow = trim( $row );
126            // Empty line
127            if ( $trimmedRow === "" ) {
128                continue;
129            }
130            // Comment
131            if ( $trimmedRow[0] === "#" ) {
132                continue;
133            }
134            // @TODO: Check user existence, if applicable
135            $this->denylist[] = $trimmedRow;
136        }
137    }
138
139    /**
140     * Checks if $userName can request a global rename
141     *
142     * @param string $userName
143     * @return bool
144     */
145    public function checkUser( string $userName ) {
146        if ( !$this->isEnabled() ) {
147            $this->logger->debug( 'GlobalRenameDenylist::checkUser() returns true, denylist is disabled' );
148            return true;
149        }
150
151        if ( $this->denylist === null ) {
152            $this->logger->debug( 'GlobalRenameDenylist::checkUser() fetches denylist, null found' );
153            $this->fetchList();
154        }
155
156        $res = true;
157        foreach ( $this->denylist as $row ) {
158            $row = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $row );
159            $regex = "/$row/u";
160            if ( !StringUtils::isValidPCRERegex( $regex ) ) {
161                // Skip invalid regex
162                continue;
163            }
164            $regexRes = preg_match( $regex, $userName );
165            if ( $regexRes === 1 ) {
166                $res = false;
167                break;
168            }
169        }
170
171        $this->logger->debug(
172            'GlobalRenameDenylist returns {result} for {username}',
173            [
174                'username' => $userName,
175                'result' => $res,
176            ]
177        );
178        return $res;
179    }
180}