2017-12-21 21:24:59 +00:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* Title Blacklist class
|
|
|
|
* @author Victor Vasiliev
|
|
|
|
* @copyright © 2007-2010 Victor Vasiliev et al
|
2018-04-07 01:23:42 +00:00
|
|
|
* @license GPL-2.0-or-later
|
2017-12-21 21:24:59 +00:00
|
|
|
* @file
|
|
|
|
*/
|
|
|
|
|
2022-04-08 13:20:08 +00:00
|
|
|
namespace MediaWiki\Extension\TitleBlacklist;
|
|
|
|
|
|
|
|
use CoreParserFunctions;
|
|
|
|
use ExtensionRegistry;
|
2024-02-10 13:23:05 +00:00
|
|
|
use MediaWiki\Config\ConfigException;
|
2022-02-11 14:18:16 +00:00
|
|
|
use MediaWiki\Extension\AntiSpoof\AntiSpoof;
|
2019-07-03 12:58:55 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
2022-02-24 21:14:12 +00:00
|
|
|
use Wikimedia\AtEase\AtEase;
|
2019-07-03 12:58:55 +00:00
|
|
|
|
2017-12-21 21:24:59 +00:00
|
|
|
/**
|
|
|
|
* @ingroup Extensions
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents a title blacklist entry
|
|
|
|
*/
|
|
|
|
class TitleBlacklistEntry {
|
|
|
|
/**
|
|
|
|
* Raw line
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $mRaw;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Regular expression to match
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $mRegex;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parameters for this entry
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private $mParams;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Entry format version
|
2019-12-12 19:15:59 +00:00
|
|
|
* @var int
|
2017-12-21 21:24:59 +00:00
|
|
|
*/
|
|
|
|
private $mFormatVersion;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Source of this entry
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $mSource;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $regex Regular expression to match
|
|
|
|
* @param array $params Parameters for this entry
|
|
|
|
* @param string $raw Raw contents of this line
|
2019-04-27 04:30:31 +00:00
|
|
|
* @param string $source
|
2017-12-21 21:24:59 +00:00
|
|
|
*/
|
|
|
|
private function __construct( $regex, $params, $raw, $source ) {
|
|
|
|
$this->mRaw = $raw;
|
|
|
|
$this->mRegex = $regex;
|
|
|
|
$this->mParams = $params;
|
|
|
|
$this->mFormatVersion = TitleBlacklist::VERSION;
|
|
|
|
$this->mSource = $source;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether this entry is capable of filtering new accounts.
|
2020-02-29 21:10:59 +00:00
|
|
|
* @return bool
|
2017-12-21 21:24:59 +00:00
|
|
|
*/
|
|
|
|
private function filtersNewAccounts() {
|
|
|
|
global $wgTitleBlacklistUsernameSources;
|
|
|
|
|
|
|
|
if ( $wgTitleBlacklistUsernameSources === '*' ) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !$wgTitleBlacklistUsernameSources ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !is_array( $wgTitleBlacklistUsernameSources ) ) {
|
2024-02-10 13:23:05 +00:00
|
|
|
throw new ConfigException(
|
2017-12-21 21:24:59 +00:00
|
|
|
'$wgTitleBlacklistUsernameSources must be "*", false or an array' );
|
|
|
|
}
|
|
|
|
|
|
|
|
return in_array( $this->mSource, $wgTitleBlacklistUsernameSources, true );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether a user can perform the specified action on the specified Title
|
|
|
|
*
|
|
|
|
* @param string $title Title to check
|
|
|
|
* @param string $action Action to check
|
2018-08-25 12:54:52 +00:00
|
|
|
* @return bool TRUE if the regex matches the title, and is not overridden
|
2017-12-21 21:24:59 +00:00
|
|
|
* else false if it doesn't match (or was overridden)
|
|
|
|
*/
|
|
|
|
public function matches( $title, $action ) {
|
|
|
|
if ( $title == '' ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( $action === 'new-account' && !$this->filtersNewAccounts() ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( isset( $this->mParams['antispoof'] )
|
2020-02-29 09:59:56 +00:00
|
|
|
&& ExtensionRegistry::getInstance()->isLoaded( 'AntiSpoof' )
|
2017-12-21 21:24:59 +00:00
|
|
|
) {
|
|
|
|
if ( $action === 'edit' ) {
|
|
|
|
// Use process cache for frequently edited pages
|
2019-07-03 12:58:55 +00:00
|
|
|
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
|
2020-02-29 09:59:56 +00:00
|
|
|
$status = $cache->getWithSetCallback(
|
2020-03-02 12:18:06 +00:00
|
|
|
$cache->makeKey( 'titleblacklist', 'normalized-unicode-status', md5( $title ) ),
|
2017-12-21 21:24:59 +00:00
|
|
|
$cache::TTL_MONTH,
|
2021-05-14 06:39:44 +00:00
|
|
|
static function () use ( $title ) {
|
2020-02-29 09:59:56 +00:00
|
|
|
return AntiSpoof::checkUnicodeStringStatus( $title );
|
2017-12-21 21:24:59 +00:00
|
|
|
},
|
|
|
|
[ 'pcTTL' => $cache::TTL_PROC_LONG ]
|
|
|
|
);
|
|
|
|
} else {
|
2020-02-29 09:59:56 +00:00
|
|
|
$status = AntiSpoof::checkUnicodeStringStatus( $title );
|
2017-12-21 21:24:59 +00:00
|
|
|
}
|
|
|
|
|
2020-02-29 09:59:56 +00:00
|
|
|
if ( $status->isOK() ) {
|
|
|
|
// Remove version from return value
|
2024-03-12 19:42:45 +00:00
|
|
|
[ , $title ] = explode( ':', $status->getValue(), 2 );
|
2017-12-21 21:24:59 +00:00
|
|
|
} else {
|
2020-02-29 09:59:56 +00:00
|
|
|
wfDebugLog( 'TitleBlacklist', 'AntiSpoof could not normalize "' . $title . '" ' .
|
|
|
|
$status->getMessage( false, false, 'en' )->text() . '.'
|
|
|
|
);
|
2017-12-21 21:24:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 21:14:12 +00:00
|
|
|
AtEase::suppressWarnings();
|
2020-12-20 04:01:58 +00:00
|
|
|
// @phan-suppress-next-line SecurityCheck-ReDoS
|
2017-12-21 21:24:59 +00:00
|
|
|
$match = preg_match(
|
|
|
|
"/^(?:{$this->mRegex})$/us" . ( isset( $this->mParams['casesensitive'] ) ? '' : 'i' ),
|
|
|
|
$title
|
|
|
|
);
|
2022-02-24 21:14:12 +00:00
|
|
|
AtEase::restoreWarnings();
|
2017-12-21 21:24:59 +00:00
|
|
|
|
|
|
|
if ( $match ) {
|
|
|
|
if ( isset( $this->mParams['moveonly'] ) && $action != 'move' ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if ( isset( $this->mParams['newaccountonly'] ) && $action != 'new-account' ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if ( !isset( $this->mParams['noedit'] ) && $action == 'edit' ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if ( isset( $this->mParams['reupload'] ) && $action == 'upload' ) {
|
|
|
|
// Special:Upload also checks 'create' permissions when not reuploading
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new TitleBlacklistEntry from a line of text
|
|
|
|
*
|
|
|
|
* @param string $line String containing a line of blacklist text
|
|
|
|
* @param string $source
|
|
|
|
* @return TitleBlacklistEntry|null
|
|
|
|
*/
|
|
|
|
public static function newFromString( $line, $source ) {
|
2022-04-08 13:20:55 +00:00
|
|
|
// Keep line for raw data
|
|
|
|
$raw = $line;
|
2017-12-21 21:24:59 +00:00
|
|
|
$options = [];
|
|
|
|
// Strip comments
|
|
|
|
$line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
|
|
|
|
$line = trim( $line );
|
|
|
|
// A blank string causes problems later on
|
|
|
|
if ( $line === '' ) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
// Parse the rest of message
|
|
|
|
$pockets = [];
|
|
|
|
if ( !preg_match( '/^(.*?)(\s*<([^<>]*)>)?$/', $line, $pockets ) ) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$regex = trim( $pockets[1] );
|
2022-04-08 13:20:55 +00:00
|
|
|
// We'll be matching against text form
|
|
|
|
$regex = str_replace( '_', ' ', $regex );
|
2022-09-29 14:12:21 +00:00
|
|
|
$opts_str = trim( $pockets[3] ?? '' );
|
2017-12-21 21:24:59 +00:00
|
|
|
// Parse opts
|
|
|
|
$opts = preg_split( '/\s*\|\s*/', $opts_str );
|
|
|
|
foreach ( $opts as $opt ) {
|
|
|
|
$opt2 = strtolower( $opt );
|
2022-09-29 14:12:21 +00:00
|
|
|
if ( in_array( $opt2, [
|
|
|
|
'antispoof',
|
|
|
|
'autoconfirmed',
|
|
|
|
'casesensitive',
|
|
|
|
'moveonly',
|
|
|
|
'newaccountonly',
|
|
|
|
'noedit',
|
|
|
|
'reupload',
|
|
|
|
] ) ) {
|
|
|
|
$options[$opt2] = true;
|
2017-12-21 21:24:59 +00:00
|
|
|
}
|
|
|
|
if ( preg_match( '/errmsg\s*=\s*(.+)/i', $opt, $matches ) ) {
|
|
|
|
$options['errmsg'] = $matches[1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Process magic words
|
|
|
|
preg_match_all( '/{{\s*([a-z]+)\s*:\s*(.+?)\s*}}/', $regex, $magicwords, PREG_SET_ORDER );
|
|
|
|
foreach ( $magicwords as $mword ) {
|
|
|
|
switch ( strtolower( $mword[1] ) ) {
|
|
|
|
case 'ns':
|
2019-10-28 20:03:37 +00:00
|
|
|
$cpf_result = CoreParserFunctions::ns(
|
|
|
|
MediaWikiServices::getInstance()->getParser(),
|
|
|
|
$mword[2]
|
|
|
|
);
|
2017-12-21 21:24:59 +00:00
|
|
|
if ( is_string( $cpf_result ) ) {
|
2021-10-14 05:48:54 +00:00
|
|
|
// All result will have the same value, so we can just use str_replace()
|
2017-12-21 21:24:59 +00:00
|
|
|
$regex = str_replace( $mword[0], $cpf_result, $regex );
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'int':
|
|
|
|
$cpf_result = wfMessage( $mword[2] )->inContentLanguage()->text();
|
|
|
|
if ( is_string( $cpf_result ) ) {
|
|
|
|
$regex = str_replace( $mword[0], $cpf_result, $regex );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-09-29 14:12:21 +00:00
|
|
|
return $regex ? new TitleBlacklistEntry( $regex, $options, $raw, $source ) : null;
|
2017-12-21 21:24:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string This entry's regular expression
|
|
|
|
*/
|
|
|
|
public function getRegex() {
|
|
|
|
return $this->mRegex;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string This entry's raw line
|
|
|
|
*/
|
|
|
|
public function getRaw() {
|
|
|
|
return $this->mRaw;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array This entry's parameters
|
|
|
|
*/
|
|
|
|
public function getParams() {
|
|
|
|
return $this->mParams;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string Custom message for this entry
|
|
|
|
*/
|
|
|
|
public function getCustomMessage() {
|
2019-03-11 03:44:59 +00:00
|
|
|
return $this->mParams['errmsg'] ?? null;
|
2017-12-21 21:24:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-12-12 19:15:59 +00:00
|
|
|
* @return int The format version
|
2017-12-21 21:24:59 +00:00
|
|
|
*/
|
|
|
|
public function getFormatVersion() {
|
|
|
|
return $this->mFormatVersion;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-12-12 19:15:59 +00:00
|
|
|
* @param int $v New version to set
|
2017-12-21 21:24:59 +00:00
|
|
|
*/
|
|
|
|
public function setFormatVersion( $v ) {
|
|
|
|
$this->mFormatVersion = $v;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the error message name for the blacklist entry.
|
|
|
|
*
|
|
|
|
* @param string $operation Operation name (as in titleblacklist-forbidden message name)
|
|
|
|
*
|
|
|
|
* @return string The error message name
|
|
|
|
*/
|
|
|
|
public function getErrorMessage( $operation ) {
|
|
|
|
$message = $this->getCustomMessage();
|
|
|
|
// For grep:
|
|
|
|
// titleblacklist-forbidden-edit, titleblacklist-forbidden-move,
|
|
|
|
// titleblacklist-forbidden-upload, titleblacklist-forbidden-new-account
|
2019-03-11 03:44:59 +00:00
|
|
|
return $message ?: "titleblacklist-forbidden-{$operation}";
|
2017-12-21 21:24:59 +00:00
|
|
|
}
|
|
|
|
}
|