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\Http\HttpRequestFactory;
26use MediaWiki\Page\WikiPageFactory;
27use MediaWiki\Title\Title;
28use Psr\Log\LoggerInterface;
29use StringUtils;
30use WikitextContent;
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    /** @var string|Title|null Source of the denylist, url to fetch it from, or null */
40    private $file = null;
41
42    /** @var string[]|null Content of the denylist */
43    private $denylist = null;
44
45    /** @var LoggerInterface */
46    private $logger;
47
48    /** @var HttpRequestFactory */
49    private $httpRequestFactory;
50
51    /** @var WikiPageFactory */
52    private $wikiPageFactory;
53
54    /**
55     * @param LoggerInterface $logger
56     * @param HttpRequestFactory $httpRequestFactory
57     * @param WikiPageFactory $wikiPageFactory
58     * @param string|Title|null $denylistSource Page with denylist, url to fetch it from,
59     *   or null for no list ($wgGlobalRenameDenylist)
60     */
61    public function __construct(
62        LoggerInterface $logger,
63        HttpRequestFactory $httpRequestFactory,
64        WikiPageFactory $wikiPageFactory,
65        $denylistSource
66    ) {
67        $this->logger = $logger;
68        $this->httpRequestFactory = $httpRequestFactory;
69        $this->wikiPageFactory = $wikiPageFactory;
70        $this->file = $denylistSource;
71    }
72
73    /**
74     * Is global rename denylist enabled?
75     *
76     * @return bool
77     */
78    private function isEnabled(): bool {
79        return $this->file !== null;
80    }
81
82    /**
83     * Internal method for fetching denylist.
84     *
85     * Denylist is fetched and parsed into denylist. Denylist source is
86     * either an URL on the internet, or a wiki page.
87     * $url has to be already set.
88     */
89    private function fetchList() {
90        if ( $this->denylist !== null && count( $this->denylist ) !== 0 ) {
91            throw new BadMethodCallException(
92                'GlobalRenameDenylist::fetchList called on already fully initialized class'
93            );
94        }
95
96        if ( $this->file instanceof Title ) {
97            $this->logger->debug( 'GlobalRenameDenylist is fetching denylist from a wikipage' );
98            $wikipage = $this->wikiPageFactory->newFromTitle( $this->file );
99            $content = $wikipage->getContent();
100            if ( $content === null ) {
101                throw new BadMethodCallException(
102                    'GlobalRenameDenylist::fetchList was called with non-existent wikipage'
103                );
104            }
105            if ( !$content instanceof WikitextContent ) {
106                throw new BadMethodCallException(
107                    'Page used with GlobalRenameDenylist has invalid content model'
108                );
109            }
110            $text = $content->getText();
111        } else {
112            $this->logger->debug( 'GlobalRenameDenylist is fetching denylist from the internet' );
113            if ( $this->file === null ) {
114                $this->logger->info( 'GlobalRenameDenylist is not specified, not fetching anything' );
115                $this->denylist = [];
116                return;
117            }
118            $text = $this->httpRequestFactory->get( $this->file, [], __METHOD__ );
119            if ( $text === null ) {
120                $this->logger->error( 'GlobalRenameDenylist failed to fetch global rename denylist.' );
121                $this->denylist = [];
122                return;
123            }
124        }
125
126        $rows = explode( "\n", $text );
127        $this->denylist = [];
128        foreach ( $rows as $row ) {
129            $trimmedRow = trim( $row );
130            // Empty line
131            if ( $trimmedRow === "" ) {
132                continue;
133            }
134            // Comment
135            if ( $trimmedRow[0] === "#" ) {
136                continue;
137            }
138            // @TODO: Check user existence, if applicable
139            $this->denylist[] = $trimmedRow;
140        }
141    }
142
143    /**
144     * Checks if $userName can request a global rename
145     *
146     * @param string $userName
147     * @return bool
148     */
149    public function checkUser( string $userName ) {
150        if ( !$this->isEnabled() ) {
151            $this->logger->debug( 'GlobalRenameDenylist::checkUser() returns true, denylist is disabled' );
152            return true;
153        }
154
155        if ( $this->denylist === null ) {
156            $this->logger->debug( 'GlobalRenameDenylist::checkUser() fetches denylist, null found' );
157            $this->fetchList();
158        }
159
160        $res = true;
161        foreach ( $this->denylist as $row ) {
162            $row = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $row );
163            $regex = "/$row/u";
164            if ( !StringUtils::isValidPCRERegex( $regex ) ) {
165                // Skip invalid regex
166                continue;
167            }
168            $regexRes = preg_match( $regex, $userName );
169            if ( $regexRes === 1 ) {
170                $res = false;
171                break;
172            }
173        }
174
175        $this->logger->debug(
176            'GlobalRenameDenylist returns {result} for {username}',
177            [
178                'username' => $userName,
179                'result' => $res,
180            ]
181        );
182        return $res;
183    }
184}