Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 103 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
TitleBlacklistEntry | |
0.00% |
0 / 103 |
|
0.00% |
0 / 11 |
1892 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
filtersNewAccounts | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
matches | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
342 | |||
newFromString | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
156 | |||
getRegex | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRaw | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getParams | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCustomMessage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFormatVersion | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setFormatVersion | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getErrorMessage | |
0.00% |
0 / 2 |
|
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 | |
10 | namespace MediaWiki\Extension\TitleBlacklist; |
11 | |
12 | use MediaWiki\Config\ConfigException; |
13 | use MediaWiki\Extension\AntiSpoof\AntiSpoof; |
14 | use MediaWiki\MediaWikiServices; |
15 | use MediaWiki\Parser\CoreParserFunctions; |
16 | use MediaWiki\Registration\ExtensionRegistry; |
17 | use Wikimedia\AtEase\AtEase; |
18 | |
19 | /** |
20 | * @ingroup Extensions |
21 | */ |
22 | |
23 | /** |
24 | * Represents a title blacklist entry |
25 | */ |
26 | class 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 | } |