mediawiki-extensions-TitleB.../TitleBlacklist.list.php
Jackmcbarn 29a2de58d2 Don't test against redirect targets
There was never any use-case given for this, nor was there a bug report
about it, and it doesn't seem useful in any way, so remove it. Also, add
comments so that it's obvious when grepping that the messages aren't
unused now.

Change-Id: I80079fb4822fbc4f47df592119b1b1f8a367927c
2014-11-30 18:58:04 +00:00

539 lines
15 KiB
PHP

<?php
/**
* Title Blacklist class
* @author Victor Vasiliev
* @copyright © 2007-2010 Victor Vasiliev et al
* @license GNU General Public License 2.0 or later
* @file
*/
//@{
/**
* @ingroup Extensions
*/
/**
* Implements a title blacklist for MediaWiki
*/
class TitleBlacklist {
private $mBlacklist = null, $mWhitelist = null;
const VERSION = 3; // Blacklist format
/**
* Get an instance of this class
*
* @return TitleBlacklist
*/
public static function singleton() {
static $instance = null;
if ( $instance === null ) {
$instance = new self;
}
return $instance;
}
/**
* Load all configured blacklist sources
*/
public function load() {
global $wgTitleBlacklistSources, $wgMemc, $wgTitleBlacklistCaching;
wfProfileIn( __METHOD__ );
// Try to find something in the cache
$cachedBlacklist = $wgMemc->get( wfMemcKey( "title_blacklist_entries" ) );
if ( is_array( $cachedBlacklist ) && count( $cachedBlacklist ) > 0 && ( $cachedBlacklist[0]->getFormatVersion() == self::VERSION ) ) {
$this->mBlacklist = $cachedBlacklist;
wfProfileOut( __METHOD__ );
return;
}
$sources = $wgTitleBlacklistSources;
$sources['local'] = array( 'type' => TBLSRC_MSG );
$this->mBlacklist = array();
foreach( $sources as $sourceName => $source ) {
$this->mBlacklist = array_merge( $this->mBlacklist, $this->parseBlacklist( $this->getBlacklistText( $source ), $sourceName ) );
}
$wgMemc->set( wfMemcKey( "title_blacklist_entries" ), $this->mBlacklist, $wgTitleBlacklistCaching['expiry'] );
wfProfileOut( __METHOD__ );
}
/**
* Load local whitelist
*/
public function loadWhitelist() {
global $wgMemc, $wgTitleBlacklistCaching;
wfProfileIn( __METHOD__ );
$cachedWhitelist = $wgMemc->get( wfMemcKey( "title_whitelist_entries" ) );
if ( is_array( $cachedWhitelist ) && count( $cachedWhitelist ) > 0 && ( $cachedWhitelist[0]->getFormatVersion() != self::VERSION ) ) {
$this->mWhitelist = $cachedWhitelist;
wfProfileOut( __METHOD__ );
return;
}
$this->mWhitelist = $this->parseBlacklist( wfMessage( 'titlewhitelist' )
->inContentLanguage()->text(), 'whitelist' );
$wgMemc->set( wfMemcKey( "title_whitelist_entries" ), $this->mWhitelist, $wgTitleBlacklistCaching['expiry'] );
wfProfileOut( __METHOD__ );
}
/**
* Get the text of a blacklist from a specified source
*
* @param $source A blacklist source from $wgTitleBlacklistSources
* @return The content of the blacklist source as a string
*/
private static function getBlacklistText( $source ) {
if ( !is_array( $source ) || count( $source ) <= 0 ) {
return ''; // Return empty string in error case
}
if ( $source['type'] == TBLSRC_MSG ) {
return wfMessage( 'titleblacklist' )->inContentLanguage()->text();
} elseif ( $source['type'] == TBLSRC_LOCALPAGE && count( $source ) >= 2 ) {
$title = Title::newFromText( $source['src'] );
if ( is_null( $title ) ) {
return '';
}
if ( $title->getNamespace() == NS_MEDIAWIKI ) {
$msg = wfMessage( $title->getText() )->inContentLanguage()->text();
if ( !wfMessage( 'titleblacklist', $msg )->isDisabled() ) {
return $msg;
} else {
return '';
}
} else {
$article = new Article( $title );
if ( $article->exists() ) {
$article->followRedirect();
return $article->getContent();
}
}
} elseif ( $source['type'] == TBLSRC_URL && count( $source ) >= 2 ) {
return self::getHttp( $source['src'] );
} elseif ( $source['type'] == TBLSRC_FILE && count( $source ) >= 2 ) {
if ( file_exists( $source['src'] ) ) {
return file_get_contents( $source['src'] );
} else {
return '';
}
}
return '';
}
/**
* Parse blacklist from a string
*
* @param $list string Text of a blacklist source
* @return array of TitleBlacklistEntry entries
*/
public static function parseBlacklist( $list, $sourceName ) {
wfProfileIn( __METHOD__ );
$lines = preg_split( "/\r?\n/", $list );
$result = array();
foreach ( $lines as $line ) {
$line = TitleBlacklistEntry :: newFromString( $line, $sourceName );
if ( $line ) {
$result[] = $line;
}
}
wfProfileOut( __METHOD__ );
return $result;
}
/**
* Check whether the blacklist restricts giver nuser
* performing a specific action on the given Title
*
* @param $title Title to check
* @param $user User to check
* @param $action string Action to check; 'edit' if unspecified
* @param $override bool If set to true, overrides work
* @return TitleBlacklistEntry|bool The corresponding TitleBlacklistEntry if
* blacklisted; otherwise false
*/
public function userCannot( $title, $user, $action = 'edit', $override = true ) {
if ( $override && self::userCanOverride( $user, $action ) ) {
return false;
} else {
$entry = $this->isBlacklisted( $title, $action );
if ( !$entry ) {
return false;
}
$params = $entry->getParams();
return isset( $params['autoconfirmed'] ) && $user->isAllowed( 'autoconfirmed' ) ? false : $entry;
}
}
/**
* Check whether the blacklist restricts
* performing a specific action on the given Title
*
* @param $title Title to check
* @param $action string Action to check; 'edit' if unspecified
* @return TitleBlacklistEntry|bool The corresponding TitleBlacklistEntry if blacklisted;
* otherwise FALSE
*/
public function isBlacklisted( $title, $action = 'edit' ) {
if ( !( $title instanceof Title ) ) {
$title = Title::newFromText( $title );
if ( !( $title instanceof Title ) ) {
// The fact that the page name is invalid will stop whatever
// action is going through. No sense in doing more work here.
return false;
}
}
$blacklist = $this->getBlacklist();
$autoconfirmedItem = false;
foreach ( $blacklist as $item ) {
if ( $item->matches( $title->getFullText(), $action ) ) {
if ( $this->isWhitelisted( $title, $action ) ) {
return false;
}
$params = $item->getParams();
if ( !isset( $params['autoconfirmed'] ) ) {
return $item;
}
if ( !$autoconfirmedItem ) {
$autoconfirmedItem = $item;
}
}
}
return $autoconfirmedItem;
}
/**
* Check whether it has been explicitly whitelisted that the
* current User may perform a specific action on the given Title
*
* @param $title Title to check
* @param $action string Action to check; 'edit' if unspecified
* @return bool True if whitelisted; otherwise false
*/
public function isWhitelisted( $title, $action = 'edit' ) {
if ( !( $title instanceof Title ) ) {
$title = Title::newFromText( $title );
}
$whitelist = $this->getWhitelist();
foreach ( $whitelist as $item ) {
if ( $item->matches( $title->getFullText(), $action ) ) {
return true;
}
}
return false;
}
/**
* Get the current blacklist
*
* @return Array of TitleBlacklistEntry items
*/
public function getBlacklist() {
if ( is_null( $this->mBlacklist ) ) {
$this->load();
}
return $this->mBlacklist;
}
/**
* Get the current whitelist
*
* @return Array of TitleBlacklistEntry items
*/
public function getWhitelist() {
if ( is_null( $this->mWhitelist ) ) {
$this->loadWhitelist();
}
return $this->mWhitelist;
}
/**
* Get the text of a blacklist source via HTTP
*
* @param $url string URL of the blacklist source
* @return string The content of the blacklist source as a string
*/
private static function getHttp( $url ) {
global $messageMemc, $wgTitleBlacklistCaching;
$key = "title_blacklist_source:" . md5( $url ); // Global shared
$warnkey = wfMemcKey( "titleblacklistwarning", md5( $url ) );
$result = $messageMemc->get( $key );
$warn = $messageMemc->get( $warnkey );
if ( !is_string( $result ) || ( !$warn && !mt_rand( 0, $wgTitleBlacklistCaching['warningchance'] ) ) ) {
$result = Http::get( $url );
$messageMemc->set( $warnkey, 1, $wgTitleBlacklistCaching['warningexpiry'] );
$messageMemc->set( $key, $result, $wgTitleBlacklistCaching['expiry'] );
}
return $result;
}
/**
* Invalidate the blacklist cache
*/
public function invalidate() {
global $wgMemc;
$wgMemc->delete( wfMemcKey( "title_blacklist_entries" ) );
}
/**
* Validate a new blacklist
*
* @param $blacklist array
* @return Array of bad entries; empty array means blacklist is valid
*/
public function validate( $blacklist ) {
$badEntries = array();
foreach ( $blacklist as $e ) {
wfSuppressWarnings();
$regex = $e->getRegex();
if ( preg_match( "/{$regex}/u", '' ) === false ) {
$badEntries[] = $e->getRaw();
}
wfRestoreWarnings();
}
return $badEntries;
}
/**
* Inidcates whether user can override blacklist on certain action.
*
* @param $action Action
*
* @return bool
*/
public static function userCanOverride( $user, $action ) {
return $user->isAllowed( 'tboverride' ) ||
( $action == 'new-account' && $user->isAllowed( 'tboverride-account' ) );
}
}
/**
* Represents a title blacklist entry
*/
class TitleBlacklistEntry {
private
$mRaw, ///< Raw line
$mRegex, ///< Regular expression to match
$mParams, ///< Parameters for this entry
$mFormatVersion, ///< Entry format version
$mSource; ///< Source of this entry
/**
* Construct a new TitleBlacklistEntry.
*
* @param $regex string Regular expression to match
* @param $params array Parameters for this entry
* @param $raw string Raw contents of this line
*/
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.
*/
private function filtersNewAccounts() {
global $wgTitleBlacklistUsernameSources;
if( $wgTitleBlacklistUsernameSources === '*' ) {
return true;
}
if( !$wgTitleBlacklistUsernameSources ) {
return false;
}
if( !is_array( $wgTitleBlacklistUsernameSources ) ) {
throw new MWException(
'$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 $title string to check
* @param $action %Action to check
* @return bool TRUE if the the regex matches the title, and is not overridden
* 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'] ) && is_callable( 'AntiSpoof::checkUnicodeString' ) ) {
list( $ok, $norm ) = AntiSpoof::checkUnicodeString( $title );
if ( $ok == "OK" ) {
list( $ver, $title ) = explode( ':', $norm, 2 );
} else {
wfDebugLog( 'TitleBlacklist', 'AntiSpoof could not normalize "' . $title . '".' );
}
}
wfSuppressWarnings();
$match = preg_match( "/^(?:{$this->mRegex})$/us" . ( isset( $this->mParams['casesensitive'] ) ? '' : 'i' ), $title );
wfRestoreWarnings();
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 $line String containing a line of blacklist text
* @return TitleBlacklistEntry
*/
public static function newFromString( $line, $source ) {
$raw = $line; // Keep line for raw data
$options = array();
// Strip comments
$line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
$line = trim( $line );
// Parse the rest of message
preg_match( '/^(.*?)(\s*<([^<>]*)>)?$/', $line, $pockets );
@list( $full, $regex, $null, $opts_str ) = $pockets;
$regex = trim( $regex );
$regex = str_replace( '_', ' ', $regex ); // We'll be matching against text form
$opts_str = trim( $opts_str );
// Parse opts
$opts = preg_split( '/\s*\|\s*/', $opts_str );
foreach ( $opts as $opt ) {
$opt2 = strtolower( $opt );
if ( $opt2 == 'autoconfirmed' ) {
$options['autoconfirmed'] = true;
}
if ( $opt2 == 'moveonly' ) {
$options['moveonly'] = true;
}
if ( $opt2 == 'newaccountonly' ) {
$options['newaccountonly'] = true;
}
if ( $opt2 == 'noedit' ) {
$options['noedit'] = true;
}
if ( $opt2 == 'casesensitive' ) {
$options['casesensitive'] = true;
}
if ( $opt2 == 'reupload' ) {
$options['reupload'] = true;
}
if ( preg_match( '/errmsg\s*=\s*(.+)/i', $opt, $matches ) ) {
$options['errmsg'] = $matches[1];
}
if ( $opt2 == 'antispoof' ) {
$options['antispoof'] = true;
}
}
// Process magic words
preg_match_all( '/{{\s*([a-z]+)\s*:\s*(.+?)\s*}}/', $regex, $magicwords, PREG_SET_ORDER );
foreach ( $magicwords as $mword ) {
global $wgParser; // Functions we're calling don't need, nevertheless let's use it
switch( strtolower( $mword[1] ) ) {
case 'ns':
$cpf_result = CoreParserFunctions::ns( $wgParser, $mword[2] );
if ( is_string( $cpf_result ) ) {
$regex = str_replace( $mword[0], $cpf_result, $regex ); // All result will have the same value, so we can just use str_seplace()
}
break;
case 'int':
$cpf_result = wfMessage( $mword[2] )->inContentLanguage()->text();
if ( is_string( $cpf_result ) ) {
$regex = str_replace( $mword[0], $cpf_result, $regex );
}
}
}
// Return result
if( $regex ) {
return new TitleBlacklistEntry( $regex, $options, $raw, $source );
} else {
return null;
}
}
/**
* @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() {
return isset( $this->mParams['errmsg'] ) ? $this->mParams['errmsg'] : null;
}
/**
* @return string The format version
*/
public function getFormatVersion() { return $this->mFormatVersion; }
/**
* Set the format version
*
* @param $v string New version to set
*/
public function setFormatVersion( $v ) { $this->mFormatVersion = $v; }
/**
* Return the error message name for the blacklist entry.
*
* @param $operation string 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
return $message ? $message : "titleblacklist-forbidden-{$operation}";
}
}
//@}