Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 135 |
TitleBlacklist | |
0.00% |
0 / 1 |
|
0.00% |
0 / 15 |
4422 | |
0.00% |
0 / 134 |
singleton | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
destroySingleton | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
load | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 18 |
|||
loadWhitelist | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 11 |
|||
getBlacklistText | |
0.00% |
0 / 1 |
272 | |
0.00% |
0 / 24 |
|||
parseBlacklist | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 7 |
|||
userCannot | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 9 |
|||
isBlacklisted | |
0.00% |
0 / 1 |
72 | |
0.00% |
0 / 16 |
|||
isWhitelisted | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 7 |
|||
getBlacklist | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
getWhitelist | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
getHttp | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 15 |
|||
invalidate | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
validate | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 8 |
|||
userCanOverride | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 2 |
<?php | |
/** | |
* Title Blacklist class | |
* @author Victor Vasiliev | |
* @copyright © 2007-2010 Victor Vasiliev et al | |
* @license GPL-2.0-or-later | |
* @file | |
*/ | |
namespace MediaWiki\Extension\TitleBlacklist; | |
use MediaWiki\MediaWikiServices; | |
use MWException; | |
use ObjectCache; | |
use TextContent; | |
use Title; | |
use User; | |
use Wikimedia\AtEase\AtEase; | |
/** | |
* @ingroup Extensions | |
*/ | |
/** | |
* Implements a title blacklist for MediaWiki | |
*/ | |
class TitleBlacklist { | |
/** @var TitleBlacklistEntry[]|null */ | |
private $mBlacklist = null; | |
/** @var TitleBlacklistEntry[]|null */ | |
private $mWhitelist = null; | |
/** @var TitleBlacklist|null */ | |
protected static $instance = null; | |
/** Blacklist format */ | |
public const VERSION = 4; | |
/** | |
* Get an instance of this class | |
* | |
* @return TitleBlacklist | |
*/ | |
public static function singleton() { | |
if ( self::$instance === null ) { | |
self::$instance = new self; | |
} | |
return self::$instance; | |
} | |
/** | |
* Destroy/reset the current singleton instance. | |
* | |
* This is solely for testing and will fail unless MW_PHPUNIT_TEST is | |
* defined. | |
*/ | |
public static function destroySingleton() { | |
if ( !defined( 'MW_PHPUNIT_TEST' ) ) { | |
throw new MWException( | |
'Can not invoke ' . __METHOD__ . '() ' . | |
'out of tests (MW_PHPUNIT_TEST not set).' | |
); | |
} | |
self::$instance = null; | |
} | |
/** | |
* Load all configured blacklist sources | |
*/ | |
public function load() { | |
global $wgTitleBlacklistSources, $wgTitleBlacklistCaching; | |
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); | |
// Try to find something in the cache | |
$cachedBlacklist = $cache->get( $cache->makeKey( 'title_blacklist_entries' ) ); | |
if ( is_array( $cachedBlacklist ) && count( $cachedBlacklist ) > 0 | |
&& ( $cachedBlacklist[0]->getFormatVersion() == self::VERSION ) | |
) { | |
$this->mBlacklist = $cachedBlacklist; | |
return; | |
} | |
$sources = $wgTitleBlacklistSources; | |
$sources['local'] = [ 'type' => 'message' ]; | |
$this->mBlacklist = []; | |
foreach ( $sources as $sourceName => $source ) { | |
$this->mBlacklist = array_merge( | |
$this->mBlacklist, | |
self::parseBlacklist( self::getBlacklistText( $source ), $sourceName ) | |
); | |
} | |
$cache->set( $cache->makeKey( 'title_blacklist_entries' ), | |
$this->mBlacklist, $wgTitleBlacklistCaching['expiry'] ); | |
wfDebugLog( 'TitleBlacklist-cache', 'Updated ' . $cache->makeKey( 'title_blacklist_entries' ) | |
. ' with ' . count( $this->mBlacklist ) . ' entries.' ); | |
} | |
/** | |
* Load local whitelist | |
*/ | |
public function loadWhitelist() { | |
global $wgTitleBlacklistCaching; | |
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); | |
$cachedWhitelist = $cache->get( $cache->makeKey( 'title_whitelist_entries' ) ); | |
if ( is_array( $cachedWhitelist ) && count( $cachedWhitelist ) > 0 | |
&& ( $cachedWhitelist[0]->getFormatVersion() != self::VERSION ) | |
) { | |
$this->mWhitelist = $cachedWhitelist; | |
return; | |
} | |
$this->mWhitelist = self::parseBlacklist( wfMessage( 'titlewhitelist' ) | |
->inContentLanguage()->text(), 'whitelist' ); | |
$cache->set( $cache->makeKey( 'title_whitelist_entries' ), | |
$this->mWhitelist, $wgTitleBlacklistCaching['expiry'] ); | |
} | |
/** | |
* Get the text of a blacklist from a specified source | |
* | |
* @param array $source A blacklist source from $wgTitleBlacklistSources | |
* @return string The content of the blacklist source as a string | |
*/ | |
private static function getBlacklistText( $source ) { | |
if ( !is_array( $source ) || count( $source ) <= 0 ) { | |
// Return empty string in error case | |
return ''; | |
} | |
if ( $source['type'] == 'message' ) { | |
return wfMessage( 'titleblacklist' )->inContentLanguage()->text(); | |
} elseif ( $source['type'] == 'localpage' && count( $source ) >= 2 ) { | |
$title = Title::newFromText( $source['src'] ); | |
if ( $title === null ) { | |
return ''; | |
} | |
if ( $title->getNamespace() == NS_MEDIAWIKI ) { | |
$msg = wfMessage( $title->getText() )->inContentLanguage(); | |
if ( !$msg->isDisabled() ) { | |
return $msg->text(); | |
} else { | |
return ''; | |
} | |
} else { | |
$page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ); | |
if ( $page->exists() ) { | |
$content = $page->getContent(); | |
return ( $content instanceof TextContent ) ? $content->getText() : ""; | |
} | |
} | |
} elseif ( $source['type'] == 'url' && count( $source ) >= 2 ) { | |
return self::getHttp( $source['src'] ); | |
} elseif ( $source['type'] == 'file' && count( $source ) >= 2 ) { | |
if ( file_exists( $source['src'] ) ) { | |
return file_get_contents( $source['src'] ); | |
} else { | |
return ''; | |
} | |
} | |
return ''; | |
} | |
/** | |
* Parse blacklist from a string | |
* | |
* @param string $list Text of a blacklist source | |
* @param string $sourceName | |
* @return TitleBlacklistEntry[] | |
*/ | |
public static function parseBlacklist( $list, $sourceName ) { | |
$lines = preg_split( "/\r?\n/", $list ); | |
$result = []; | |
foreach ( $lines as $line ) { | |
$entry = TitleBlacklistEntry::newFromString( $line, $sourceName ); | |
if ( $entry ) { | |
$result[] = $entry; | |
} | |
} | |
return $result; | |
} | |
/** | |
* Check whether the blacklist restricts given user | |
* performing a specific action on the given Title | |
* | |
* @param Title $title Title to check | |
* @param User $user User to check | |
* @param string $action Action to check; 'edit' if unspecified | |
* @param bool $override 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 ) { | |
$entry = $this->isBlacklisted( $title, $action ); | |
if ( !$entry ) { | |
return false; | |
} | |
$params = $entry->getParams(); | |
if ( isset( $params['autoconfirmed'] ) && $user->isAllowed( 'autoconfirmed' ) ) { | |
return false; | |
} | |
if ( $override && self::userCanOverride( $user, $action ) ) { | |
return false; | |
} | |
return $entry; | |
} | |
/** | |
* Check whether the blacklist restricts | |
* performing a specific action on the given Title | |
* | |
* @param Title $title Title to check | |
* @param string $action 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 Title to check | |
* @param string $action 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 TitleBlacklistEntry[] | |
*/ | |
public function getBlacklist() { | |
if ( $this->mBlacklist === null ) { | |
$this->load(); | |
} | |
return $this->mBlacklist; | |
} | |
/** | |
* Get the current whitelist | |
* | |
* @return TitleBlacklistEntry[] | |
*/ | |
public function getWhitelist() { | |
if ( $this->mWhitelist === null ) { | |
$this->loadWhitelist(); | |
} | |
return $this->mWhitelist; | |
} | |
/** | |
* Get the text of a blacklist source via HTTP | |
* | |
* @param string $url URL of the blacklist source | |
* @return string The content of the blacklist source as a string | |
*/ | |
private static function getHttp( $url ) { | |
global $wgTitleBlacklistCaching, $wgMessageCacheType; | |
// FIXME: This is a hack to use Memcached where possible (incl. WMF), | |
// but have CACHE_DB as fallback (instead of no cache). | |
// This might be a good candidate for T248005. | |
$cache = ObjectCache::getInstance( $wgMessageCacheType ); | |
// Globally shared | |
$key = $cache->makeGlobalKey( 'title_blacklist_source', md5( $url ) ); | |
// Per-wiki | |
$warnkey = $cache->makeKey( 'titleblacklistwarning', md5( $url ) ); | |
$result = $cache->get( $key ); | |
$warn = $cache->get( $warnkey ); | |
if ( !is_string( $result ) | |
|| ( !$warn && !mt_rand( 0, $wgTitleBlacklistCaching['warningchance'] ) ) | |
) { | |
$result = MediaWikiServices::getInstance()->getHttpRequestFactory() | |
->get( $url, [], __METHOD__ ); | |
$cache->set( $warnkey, 1, $wgTitleBlacklistCaching['warningexpiry'] ); | |
$cache->set( $key, $result, $wgTitleBlacklistCaching['expiry'] ); | |
if ( !$result ) { | |
wfDebugLog( 'TitleBlacklist-cache', "Error loading title blacklist from $url\n" ); | |
$result = ''; | |
} | |
} | |
return $result; | |
} | |
/** | |
* Invalidate the blacklist cache | |
*/ | |
public function invalidate() { | |
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); | |
$cache->delete( $cache->makeKey( 'title_blacklist_entries' ) ); | |
} | |
/** | |
* Validate a new blacklist | |
* | |
* @suppress PhanParamSuspiciousOrder The preg_match() params are in the correct order | |
* @param TitleBlacklistEntry[] $blacklist | |
* @return string[] List of invalid entries; empty array means blacklist is valid | |
*/ | |
public function validate( array $blacklist ) { | |
$badEntries = []; | |
foreach ( $blacklist as $e ) { | |
AtEase::suppressWarnings(); | |
$regex = $e->getRegex(); | |
// @phan-suppress-next-line SecurityCheck-ReDoS | |
if ( preg_match( "/{$regex}/u", '' ) === false ) { | |
$badEntries[] = $e->getRaw(); | |
} | |
AtEase::restoreWarnings(); | |
} | |
return $badEntries; | |
} | |
/** | |
* Indicates whether user can override blacklist on certain action. | |
* | |
* @param User $user | |
* @param string $action Action | |
* | |
* @return bool | |
*/ | |
public static function userCanOverride( $user, $action ) { | |
return $user->isAllowed( 'tboverride' ) || | |
( $action == 'new-account' && $user->isAllowed( 'tboverride-account' ) ); | |
} | |
} | |
class_alias( TitleBlacklist::class, 'TitleBlacklist' ); |