Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 103
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
TitleBlacklistEntry
0.00% covered (danger)
0.00%
0 / 103
0.00% covered (danger)
0.00%
0 / 11
1892
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 filtersNewAccounts
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 matches
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
342
 newFromString
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
156
 getRegex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRaw
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCustomMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFormatVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setFormatVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getErrorMessage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Title Blacklist class
4 * @author Victor Vasiliev
5 * @copyright © 2007-2010 Victor Vasiliev et al
6 * @license GPL-2.0-or-later
7 * @file
8 */
9
10namespace MediaWiki\Extension\TitleBlacklist;
11
12use CoreParserFunctions;
13use ExtensionRegistry;
14use MediaWiki\Config\ConfigException;
15use MediaWiki\Extension\AntiSpoof\AntiSpoof;
16use MediaWiki\MediaWikiServices;
17use Wikimedia\AtEase\AtEase;
18
19/**
20 * @ingroup Extensions
21 */
22
23/**
24 * Represents a title blacklist entry
25 */
26class TitleBlacklistEntry {
27    /**
28     * Raw line
29     * @var string
30     */
31    private $mRaw;
32
33    /**
34     * Regular expression to match
35     * @var string
36     */
37    private $mRegex;
38
39    /**
40     * Parameters for this entry
41     * @var array
42     */
43    private $mParams;
44
45    /**
46     * Entry format version
47     * @var int
48     */
49    private $mFormatVersion;
50
51    /**
52     * Source of this entry
53     * @var string
54     */
55    private $mSource;
56
57    /**
58     * @param string $regex Regular expression to match
59     * @param array $params Parameters for this entry
60     * @param string $raw Raw contents of this line
61     * @param string $source
62     */
63    private function __construct( $regex, $params, $raw, $source ) {
64        $this->mRaw = $raw;
65        $this->mRegex = $regex;
66        $this->mParams = $params;
67        $this->mFormatVersion = TitleBlacklist::VERSION;
68        $this->mSource = $source;
69    }
70
71    /**
72     * Returns whether this entry is capable of filtering new accounts.
73     * @return bool
74     */
75    private function filtersNewAccounts() {
76        global $wgTitleBlacklistUsernameSources;
77
78        if ( $wgTitleBlacklistUsernameSources === '*' ) {
79            return true;
80        }
81
82        if ( !$wgTitleBlacklistUsernameSources ) {
83            return false;
84        }
85
86        if ( !is_array( $wgTitleBlacklistUsernameSources ) ) {
87            throw new ConfigException(
88                '$wgTitleBlacklistUsernameSources must be "*", false or an array' );
89        }
90
91        return in_array( $this->mSource, $wgTitleBlacklistUsernameSources, true );
92    }
93
94    /**
95     * Check whether a user can perform the specified action on the specified Title
96     *
97     * @param string $title Title to check
98     * @param string $action Action to check
99     * @return bool TRUE if the regex matches the title, and is not overridden
100     * else false if it doesn't match (or was overridden)
101     */
102    public function matches( $title, $action ) {
103        if ( $title == '' ) {
104            return false;
105        }
106
107        if ( $action === 'new-account' && !$this->filtersNewAccounts() ) {
108            return false;
109        }
110
111        if ( isset( $this->mParams['antispoof'] )
112            && ExtensionRegistry::getInstance()->isLoaded( 'AntiSpoof' )
113        ) {
114            if ( $action === 'edit' ) {
115                // Use process cache for frequently edited pages
116                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
117                $status = $cache->getWithSetCallback(
118                    $cache->makeKey( 'titleblacklist', 'normalized-unicode-status', md5( $title ) ),
119                    $cache::TTL_MONTH,
120                    static function () use ( $title ) {
121                        return AntiSpoof::checkUnicodeStringStatus( $title );
122                    },
123                    [ 'pcTTL' => $cache::TTL_PROC_LONG ]
124                );
125            } else {
126                $status = AntiSpoof::checkUnicodeStringStatus( $title );
127            }
128
129            if ( $status->isOK() ) {
130                // Remove version from return value
131                [ , $title ] = explode( ':', $status->getValue(), 2 );
132            } else {
133                wfDebugLog( 'TitleBlacklist', 'AntiSpoof could not normalize "' . $title . '" ' .
134                    $status->getMessage( false, false, 'en' )->text() . '.'
135                );
136            }
137        }
138
139        AtEase::suppressWarnings();
140        // @phan-suppress-next-line SecurityCheck-ReDoS
141        $match = preg_match(
142            "/^(?:{$this->mRegex})$/us" . ( isset( $this->mParams['casesensitive'] ) ? '' : 'i' ),
143            $title
144        );
145        AtEase::restoreWarnings();
146
147        if ( $match ) {
148            if ( isset( $this->mParams['moveonly'] ) && $action != 'move' ) {
149                return false;
150            }
151            if ( isset( $this->mParams['newaccountonly'] ) && $action != 'new-account' ) {
152                return false;
153            }
154            if ( !isset( $this->mParams['noedit'] ) && $action == 'edit' ) {
155                return false;
156            }
157            if ( isset( $this->mParams['reupload'] ) && $action == 'upload' ) {
158                // Special:Upload also checks 'create' permissions when not reuploading
159                return false;
160            }
161            return true;
162        }
163
164        return false;
165    }
166
167    /**
168     * Create a new TitleBlacklistEntry from a line of text
169     *
170     * @param string $line String containing a line of blacklist text
171     * @param string $source
172     * @return TitleBlacklistEntry|null
173     */
174    public static function newFromString( $line, $source ) {
175        // Keep line for raw data
176        $raw = $line;
177        $options = [];
178        // Strip comments
179        $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
180        $line = trim( $line );
181        // A blank string causes problems later on
182        if ( $line === '' ) {
183            return null;
184        }
185        // Parse the rest of message
186        $pockets = [];
187        if ( !preg_match( '/^(.*?)(\s*<([^<>]*)>)?$/', $line, $pockets ) ) {
188            return null;
189        }
190        $regex = trim( $pockets[1] );
191        // We'll be matching against text form
192        $regex = str_replace( '_', ' ', $regex );
193        $opts_str = trim( $pockets[3] ?? '' );
194        // Parse opts
195        $opts = preg_split( '/\s*\|\s*/', $opts_str );
196        foreach ( $opts as $opt ) {
197            $opt2 = strtolower( $opt );
198            if ( in_array( $opt2, [
199                'antispoof',
200                'autoconfirmed',
201                'casesensitive',
202                'moveonly',
203                'newaccountonly',
204                'noedit',
205                'reupload',
206            ] ) ) {
207                $options[$opt2] = true;
208            }
209            if ( preg_match( '/errmsg\s*=\s*(.+)/i', $opt, $matches ) ) {
210                $options['errmsg'] = $matches[1];
211            }
212        }
213        // Process magic words
214        preg_match_all( '/{{\s*([a-z]+)\s*:\s*(.+?)\s*}}/', $regex, $magicwords, PREG_SET_ORDER );
215        foreach ( $magicwords as $mword ) {
216            switch ( strtolower( $mword[1] ) ) {
217                case 'ns':
218                    $cpf_result = CoreParserFunctions::ns(
219                        MediaWikiServices::getInstance()->getParser(),
220                        $mword[2]
221                    );
222                    if ( is_string( $cpf_result ) ) {
223                        // All result will have the same value, so we can just use str_replace()
224                        $regex = str_replace( $mword[0], $cpf_result, $regex );
225                    }
226                    break;
227                case 'int':
228                    $cpf_result = wfMessage( $mword[2] )->inContentLanguage()->text();
229                    if ( is_string( $cpf_result ) ) {
230                        $regex = str_replace( $mword[0], $cpf_result, $regex );
231                    }
232            }
233        }
234        return $regex ? new TitleBlacklistEntry( $regex, $options, $raw, $source ) : null;
235    }
236
237    /**
238     * @return string This entry's regular expression
239     */
240    public function getRegex() {
241        return $this->mRegex;
242    }
243
244    /**
245     * @return string This entry's raw line
246     */
247    public function getRaw() {
248        return $this->mRaw;
249    }
250
251    /**
252     * @return array This entry's parameters
253     */
254    public function getParams() {
255        return $this->mParams;
256    }
257
258    /**
259     * @return string Custom message for this entry
260     */
261    public function getCustomMessage() {
262        return $this->mParams['errmsg'] ?? null;
263    }
264
265    /**
266     * @return int The format version
267     */
268    public function getFormatVersion() {
269        return $this->mFormatVersion;
270    }
271
272    /**
273     * @param int $v New version to set
274     */
275    public function setFormatVersion( $v ) {
276        $this->mFormatVersion = $v;
277    }
278
279    /**
280     * Return the error message name for the blacklist entry.
281     *
282     * @param string $operation Operation name (as in titleblacklist-forbidden message name)
283     *
284     * @return string The error message name
285     */
286    public function getErrorMessage( $operation ) {
287        $message = $this->getCustomMessage();
288        // For grep:
289        // titleblacklist-forbidden-edit, titleblacklist-forbidden-move,
290        // titleblacklist-forbidden-upload, titleblacklist-forbidden-new-account
291        return $message ?: "titleblacklist-forbidden-{$operation}";
292    }
293}