[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Title Blacklist class 4 * @author Victor Vasiliev 5 * @copyright © 2007-2010 Victor Vasiliev et al 6 * @license GNU General Public License 2.0 or later 7 * @file 8 */ 9 10 //@{ 11 /** 12 * @ingroup Extensions 13 */ 14 15 /** 16 * Implements a title blacklist for MediaWiki 17 */ 18 class TitleBlacklist { 19 private $mBlacklist = null, $mWhitelist = null; 20 const VERSION = 3; // Blacklist format 21 22 /** 23 * Get an instance of this class 24 * 25 * @return TitleBlacklist 26 */ 27 public static function singleton() { 28 static $instance = null; 29 30 if ( $instance === null ) { 31 $instance = new self; 32 } 33 return $instance; 34 } 35 36 /** 37 * Load all configured blacklist sources 38 */ 39 public function load() { 40 global $wgTitleBlacklistSources, $wgMemc, $wgTitleBlacklistCaching; 41 wfProfileIn( __METHOD__ ); 42 // Try to find something in the cache 43 $cachedBlacklist = $wgMemc->get( wfMemcKey( "title_blacklist_entries" ) ); 44 if ( is_array( $cachedBlacklist ) && count( $cachedBlacklist ) > 0 && ( $cachedBlacklist[0]->getFormatVersion() == self::VERSION ) ) { 45 $this->mBlacklist = $cachedBlacklist; 46 wfProfileOut( __METHOD__ ); 47 return; 48 } 49 50 $sources = $wgTitleBlacklistSources; 51 $sources['local'] = array( 'type' => TBLSRC_MSG ); 52 $this->mBlacklist = array(); 53 foreach( $sources as $sourceName => $source ) { 54 $this->mBlacklist = array_merge( $this->mBlacklist, $this->parseBlacklist( $this->getBlacklistText( $source ), $sourceName ) ); 55 } 56 $wgMemc->set( wfMemcKey( "title_blacklist_entries" ), $this->mBlacklist, $wgTitleBlacklistCaching['expiry'] ); 57 wfProfileOut( __METHOD__ ); 58 } 59 60 /** 61 * Load local whitelist 62 */ 63 public function loadWhitelist() { 64 global $wgMemc, $wgTitleBlacklistCaching; 65 wfProfileIn( __METHOD__ ); 66 $cachedWhitelist = $wgMemc->get( wfMemcKey( "title_whitelist_entries" ) ); 67 if ( is_array( $cachedWhitelist ) && count( $cachedWhitelist ) > 0 && ( $cachedWhitelist[0]->getFormatVersion() != self::VERSION ) ) { 68 $this->mWhitelist = $cachedWhitelist; 69 wfProfileOut( __METHOD__ ); 70 return; 71 } 72 $this->mWhitelist = $this->parseBlacklist( wfMessage( 'titlewhitelist' ) 73 ->inContentLanguage()->text(), 'whitelist' ); 74 $wgMemc->set( wfMemcKey( "title_whitelist_entries" ), $this->mWhitelist, $wgTitleBlacklistCaching['expiry'] ); 75 wfProfileOut( __METHOD__ ); 76 } 77 78 /** 79 * Get the text of a blacklist from a specified source 80 * 81 * @param $source A blacklist source from $wgTitleBlacklistSources 82 * @return The content of the blacklist source as a string 83 */ 84 private static function getBlacklistText( $source ) { 85 if ( !is_array( $source ) || count( $source ) <= 0 ) { 86 return ''; // Return empty string in error case 87 } 88 89 if ( $source['type'] == TBLSRC_MSG ) { 90 return wfMessage( 'titleblacklist' )->inContentLanguage()->text(); 91 } elseif ( $source['type'] == TBLSRC_LOCALPAGE && count( $source ) >= 2 ) { 92 $title = Title::newFromText( $source['src'] ); 93 if ( is_null( $title ) ) { 94 return ''; 95 } 96 if ( $title->getNamespace() == NS_MEDIAWIKI ) { 97 $msg = wfMessage( $title->getText() )->inContentLanguage()->text(); 98 if ( !wfMessage( 'titleblacklist', $msg )->isDisabled() ) { 99 return $msg; 100 } else { 101 return ''; 102 } 103 } else { 104 $article = new Article( $title ); 105 if ( $article->exists() ) { 106 $article->followRedirect(); 107 return $article->getContent(); 108 } 109 } 110 } elseif ( $source['type'] == TBLSRC_URL && count( $source ) >= 2 ) { 111 return self::getHttp( $source['src'] ); 112 } elseif ( $source['type'] == TBLSRC_FILE && count( $source ) >= 2 ) { 113 if ( file_exists( $source['src'] ) ) { 114 return file_get_contents( $source['src'] ); 115 } else { 116 return ''; 117 } 118 } 119 120 return ''; 121 } 122 123 /** 124 * Parse blacklist from a string 125 * 126 * @param $list string Text of a blacklist source 127 * @return array of TitleBlacklistEntry entries 128 */ 129 public static function parseBlacklist( $list, $sourceName ) { 130 wfProfileIn( __METHOD__ ); 131 $lines = preg_split( "/\r?\n/", $list ); 132 $result = array(); 133 foreach ( $lines as $line ) { 134 $line = TitleBlacklistEntry :: newFromString( $line, $sourceName ); 135 if ( $line ) { 136 $result[] = $line; 137 } 138 } 139 140 wfProfileOut( __METHOD__ ); 141 return $result; 142 } 143 144 /** 145 * Check whether the blacklist restricts giver nuser 146 * performing a specific action on the given Title 147 * 148 * @param $title Title to check 149 * @param $user User to check 150 * @param $action string Action to check; 'edit' if unspecified 151 * @param $override bool If set to true, overrides work 152 * @return TitleBlacklistEntry|bool The corresponding TitleBlacklistEntry if 153 * blacklisted; otherwise false 154 */ 155 public function userCannot( $title, $user, $action = 'edit', $override = true ) { 156 if ( $override && self::userCanOverride( $user, $action ) ) { 157 return false; 158 } else { 159 $entry = $this->isBlacklisted( $title, $action ); 160 if ( !$entry ) { 161 return false; 162 } 163 $params = $entry->getParams(); 164 return isset( $params['autoconfirmed'] ) && $user->isAllowed( 'autoconfirmed' ) ? false : $entry; 165 } 166 } 167 168 /** 169 * Check whether the blacklist restricts 170 * performing a specific action on the given Title 171 * 172 * @param $title Title to check 173 * @param $action string Action to check; 'edit' if unspecified 174 * @return TitleBlacklistEntry|bool The corresponding TitleBlacklistEntry if blacklisted; 175 * otherwise FALSE 176 */ 177 public function isBlacklisted( $title, $action = 'edit' ) { 178 if ( !( $title instanceof Title ) ) { 179 $title = Title::newFromText( $title ); 180 if ( !( $title instanceof Title ) ) { 181 // The fact that the page name is invalid will stop whatever 182 // action is going through. No sense in doing more work here. 183 return false; 184 } 185 } 186 $blacklist = $this->getBlacklist(); 187 $autoconfirmedItem = false; 188 foreach ( $blacklist as $item ) { 189 if ( $item->matches( $title->getFullText(), $action ) ) { 190 if ( $this->isWhitelisted( $title, $action ) ) { 191 return false; 192 } 193 $params = $item->getParams(); 194 if ( !isset( $params['autoconfirmed'] ) ) { 195 return $item; 196 } 197 if ( !$autoconfirmedItem ) { 198 $autoconfirmedItem = $item; 199 } 200 } 201 } 202 return $autoconfirmedItem; 203 } 204 205 /** 206 * Check whether it has been explicitly whitelisted that the 207 * current User may perform a specific action on the given Title 208 * 209 * @param $title Title to check 210 * @param $action string Action to check; 'edit' if unspecified 211 * @return bool True if whitelisted; otherwise false 212 */ 213 public function isWhitelisted( $title, $action = 'edit' ) { 214 if ( !( $title instanceof Title ) ) { 215 $title = Title::newFromText( $title ); 216 } 217 $whitelist = $this->getWhitelist(); 218 foreach ( $whitelist as $item ) { 219 if ( $item->matches( $title->getFullText(), $action ) ) { 220 return true; 221 } 222 } 223 return false; 224 } 225 226 /** 227 * Get the current blacklist 228 * 229 * @return Array of TitleBlacklistEntry items 230 */ 231 public function getBlacklist() { 232 if ( is_null( $this->mBlacklist ) ) { 233 $this->load(); 234 } 235 return $this->mBlacklist; 236 } 237 238 /** 239 * Get the current whitelist 240 * 241 * @return Array of TitleBlacklistEntry items 242 */ 243 public function getWhitelist() { 244 if ( is_null( $this->mWhitelist ) ) { 245 $this->loadWhitelist(); 246 } 247 return $this->mWhitelist; 248 } 249 250 /** 251 * Get the text of a blacklist source via HTTP 252 * 253 * @param $url string URL of the blacklist source 254 * @return string The content of the blacklist source as a string 255 */ 256 private static function getHttp( $url ) { 257 global $messageMemc, $wgTitleBlacklistCaching; 258 $key = "title_blacklist_source:" . md5( $url ); // Global shared 259 $warnkey = wfMemcKey( "titleblacklistwarning", md5( $url ) ); 260 $result = $messageMemc->get( $key ); 261 $warn = $messageMemc->get( $warnkey ); 262 if ( !is_string( $result ) || ( !$warn && !mt_rand( 0, $wgTitleBlacklistCaching['warningchance'] ) ) ) { 263 $result = Http::get( $url ); 264 $messageMemc->set( $warnkey, 1, $wgTitleBlacklistCaching['warningexpiry'] ); 265 $messageMemc->set( $key, $result, $wgTitleBlacklistCaching['expiry'] ); 266 } 267 return $result; 268 } 269 270 /** 271 * Invalidate the blacklist cache 272 */ 273 public function invalidate() { 274 global $wgMemc; 275 $wgMemc->delete( wfMemcKey( "title_blacklist_entries" ) ); 276 } 277 278 /** 279 * Validate a new blacklist 280 * 281 * @param $blacklist array 282 * @return Array of bad entries; empty array means blacklist is valid 283 */ 284 public function validate( $blacklist ) { 285 $badEntries = array(); 286 foreach ( $blacklist as $e ) { 287 wfSuppressWarnings(); 288 $regex = $e->getRegex(); 289 if ( preg_match( "/{$regex}/u", '' ) === false ) { 290 $badEntries[] = $e->getRaw(); 291 } 292 wfRestoreWarnings(); 293 } 294 return $badEntries; 295 } 296 297 /** 298 * Inidcates whether user can override blacklist on certain action. 299 * 300 * @param $action Action 301 * 302 * @return bool 303 */ 304 public static function userCanOverride( $user, $action ) { 305 return $user->isAllowed( 'tboverride' ) || 306 ( $action == 'new-account' && $user->isAllowed( 'tboverride-account' ) ); 307 } 308 } 309 310 311 /** 312 * Represents a title blacklist entry 313 */ 314 class TitleBlacklistEntry { 315 private 316 $mRaw, ///< Raw line 317 $mRegex, ///< Regular expression to match 318 $mParams, ///< Parameters for this entry 319 $mFormatVersion, ///< Entry format version 320 $mSource; ///< Source of this entry 321 322 /** 323 * Construct a new TitleBlacklistEntry. 324 * 325 * @param $regex string Regular expression to match 326 * @param $params array Parameters for this entry 327 * @param $raw string Raw contents of this line 328 */ 329 private function __construct( $regex, $params, $raw, $source ) { 330 $this->mRaw = $raw; 331 $this->mRegex = $regex; 332 $this->mParams = $params; 333 $this->mFormatVersion = TitleBlacklist::VERSION; 334 $this->mSource = $source; 335 } 336 337 /** 338 * Returns whether this entry is capable of filtering new accounts. 339 */ 340 private function filtersNewAccounts() { 341 global $wgTitleBlacklistUsernameSources; 342 343 if( $wgTitleBlacklistUsernameSources === '*' ) { 344 return true; 345 } 346 347 if( !$wgTitleBlacklistUsernameSources ) { 348 return false; 349 } 350 351 if( !is_array( $wgTitleBlacklistUsernameSources ) ) { 352 throw new MWException( 353 '$wgTitleBlacklistUsernameSources must be "*", false or an array' ); 354 } 355 356 return in_array( $this->mSource, $wgTitleBlacklistUsernameSources, true ); 357 } 358 359 /** 360 * Check whether a user can perform the specified action 361 * on the specified Title 362 * 363 * @param $title string to check 364 * @param $action %Action to check 365 * @return bool TRUE if the the regex matches the title, and is not overridden 366 * else false if it doesn't match (or was overridden) 367 */ 368 public function matches( $title, $action ) { 369 if ( !$title ) { 370 return false; 371 } 372 373 if( $action == 'new-account' && !$this->filtersNewAccounts() ) { 374 return false; 375 } 376 377 if ( isset( $this->mParams['antispoof'] ) && is_callable( 'AntiSpoof::checkUnicodeString' ) ) { 378 list( $ok, $norm ) = AntiSpoof::checkUnicodeString( $title ); 379 if ( $ok == "OK" ) { 380 list( $ver, $title ) = explode( ':', $norm, 2 ); 381 } else { 382 wfDebugLog( 'TitleBlacklist', 'AntiSpoof could not normalize "' . $title . '".' ); 383 } 384 } 385 386 wfSuppressWarnings(); 387 $match = preg_match( "/^(?:{$this->mRegex})$/us" . ( isset( $this->mParams['casesensitive'] ) ? '' : 'i' ), $title ); 388 wfRestoreWarnings(); 389 390 if ( $match ) { 391 if ( isset( $this->mParams['moveonly'] ) && $action != 'move' ) { 392 return false; 393 } 394 if ( isset( $this->mParams['newaccountonly'] ) && $action != 'new-account' ) { 395 return false; 396 } 397 if ( !isset( $this->mParams['noedit'] ) && $action == 'edit' ) { 398 return false; 399 } 400 if ( isset( $this->mParams['reupload'] ) && $action == 'upload' ) { 401 // Special:Upload also checks 'create' permissions when not reuploading 402 return false; 403 } 404 return true; 405 } 406 return false; 407 } 408 409 /** 410 * Create a new TitleBlacklistEntry from a line of text 411 * 412 * @param $line String containing a line of blacklist text 413 * @return TitleBlacklistEntry 414 */ 415 public static function newFromString( $line, $source ) { 416 $raw = $line; // Keep line for raw data 417 $options = array(); 418 // Strip comments 419 $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line ); 420 $line = trim( $line ); 421 // Parse the rest of message 422 preg_match( '/^(.*?)(\s*<([^<>]*)>)?$/', $line, $pockets ); 423 @list( $full, $regex, $null, $opts_str ) = $pockets; 424 $regex = trim( $regex ); 425 $regex = str_replace( '_', ' ', $regex ); // We'll be matching against text form 426 $opts_str = trim( $opts_str ); 427 // Parse opts 428 $opts = preg_split( '/\s*\|\s*/', $opts_str ); 429 foreach ( $opts as $opt ) { 430 $opt2 = strtolower( $opt ); 431 if ( $opt2 == 'autoconfirmed' ) { 432 $options['autoconfirmed'] = true; 433 } 434 if ( $opt2 == 'moveonly' ) { 435 $options['moveonly'] = true; 436 } 437 if ( $opt2 == 'newaccountonly' ) { 438 $options['newaccountonly'] = true; 439 } 440 if ( $opt2 == 'noedit' ) { 441 $options['noedit'] = true; 442 } 443 if ( $opt2 == 'casesensitive' ) { 444 $options['casesensitive'] = true; 445 } 446 if ( $opt2 == 'reupload' ) { 447 $options['reupload'] = true; 448 } 449 if ( preg_match( '/errmsg\s*=\s*(.+)/i', $opt, $matches ) ) { 450 $options['errmsg'] = $matches[1]; 451 } 452 if ( $opt2 == 'antispoof' ) { 453 $options['antispoof'] = true; 454 } 455 } 456 // Process magic words 457 preg_match_all( '/{{\s*([a-z]+)\s*:\s*(.+?)\s*}}/', $regex, $magicwords, PREG_SET_ORDER ); 458 foreach ( $magicwords as $mword ) { 459 global $wgParser; // Functions we're calling don't need, nevertheless let's use it 460 switch( strtolower( $mword[1] ) ) { 461 case 'ns': 462 $cpf_result = CoreParserFunctions::ns( $wgParser, $mword[2] ); 463 if ( is_string( $cpf_result ) ) { 464 $regex = str_replace( $mword[0], $cpf_result, $regex ); // All result will have the same value, so we can just use str_seplace() 465 } 466 break; 467 case 'int': 468 $cpf_result = wfMessage( $mword[2] )->inContentLanguage()->text(); 469 if ( is_string( $cpf_result ) ) { 470 $regex = str_replace( $mword[0], $cpf_result, $regex ); 471 } 472 } 473 } 474 // Return result 475 if( $regex ) { 476 return new TitleBlacklistEntry( $regex, $options, $raw, $source ); 477 } else { 478 return null; 479 } 480 } 481 482 /** 483 * @return string This entry's regular expression 484 */ 485 public function getRegex() { 486 return $this->mRegex; 487 } 488 489 /** 490 * @return string This entry's raw line 491 */ 492 public function getRaw() { 493 return $this->mRaw; 494 } 495 496 /** 497 * @return array This entry's parameters 498 */ 499 public function getParams() { 500 return $this->mParams; 501 } 502 503 /** 504 * @return string Custom message for this entry 505 */ 506 public function getCustomMessage() { 507 return isset( $this->mParams['errmsg'] ) ? $this->mParams['errmsg'] : null; 508 } 509 510 /** 511 * @return string The format version 512 */ 513 public function getFormatVersion() { return $this->mFormatVersion; } 514 515 /** 516 * Set the format version 517 * 518 * @param $v string New version to set 519 */ 520 public function setFormatVersion( $v ) { $this->mFormatVersion = $v; } 521 522 /** 523 * Return the error message name for the blacklist entry. 524 * 525 * @param $operation string Operation name (as in titleblacklist-forbidden message name) 526 * 527 * @return string The error message name 528 */ 529 public function getErrorMessage( $operation ) { 530 $message = $this->getCustomMessage(); 531 return $message ? $message : "titleblacklist-forbidden-{$operation}"; 532 } 533 } 534 535 //@}
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |