wiki.techinc.nl/includes/Permissions/RestrictionStore.php
James D. Forrester 8a659ca2fe ILoadBalancer/ILBFactory: Rename hasOrMadeRecentMasterChanges to hasOrMadeRecentPrimaryChanges
Bug: T282894
Change-Id: I1d6130bcd09019f9e2de2974878902c7aafe8f0a
2021-09-02 16:34:44 -07:00

751 lines
24 KiB
PHP

<?php
namespace MediaWiki\Permissions;
use CommentStore;
use DBAccessObjectUtils;
use IDBAccessObject;
use LinkCache;
use MediaWiki\Cache\CacheKeyHelper;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Page\PageIdentity;
use MediaWiki\Page\PageIdentityValue;
use MediaWiki\Page\PageStore;
use stdClass;
use Title;
use WANObjectCache;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
/**
* Class RestrictionStore
*
* @since 1.37
*/
class RestrictionStore {
/** @internal */
public const CONSTRUCTOR_OPTIONS = [
'NamespaceProtection',
'RestrictionLevels',
'RestrictionTypes',
'SemiprotectedRestrictionLevels',
];
/** @var ServiceOptions */
private $options;
/** @var WANObjectCache */
private $wanCache;
/** @var ILoadBalancer */
private $loadBalancer;
/** @var LinkCache */
private $linkCache;
/** @var CommentStore */
private $commentStore;
/** @var HookContainer */
private $hookContainer;
/** @var HookRunner */
private $hookRunner;
/** @var PageStore */
private $pageStore;
/**
* @var array[] Caching various restrictions data in the following format:
* cache key => [
* string[] `restrictions` => restrictions loaded for pages
* string `oldRestrictions` => legacy-formatted restrictions from page.page_restrictions
* ?string `expiry` => restrictions expiry data for pages
* ?array `create_protection` => value for getCreateProtection
* bool `cascade` => cascade restrictions on this page to included templates and images?
* array[] `cascade_sources` => the results of getCascadeProtectionSources
* bool `has_cascading` => Are cascading restrictions in effect on this page?
* ]
*/
private $cache = [];
/**
* @param ServiceOptions $options
* @param WANObjectCache $wanCache
* @param ILoadBalancer $loadBalancer
* @param LinkCache $linkCache
* @param CommentStore $commentStore
* @param HookContainer $hookContainer
* @param PageStore $pageStore
*/
public function __construct(
ServiceOptions $options,
WANObjectCache $wanCache,
ILoadBalancer $loadBalancer,
LinkCache $linkCache,
CommentStore $commentStore,
HookContainer $hookContainer,
PageStore $pageStore
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $options;
$this->wanCache = $wanCache;
$this->loadBalancer = $loadBalancer;
$this->linkCache = $linkCache;
$this->commentStore = $commentStore;
$this->hookContainer = $hookContainer;
$this->hookRunner = new HookRunner( $hookContainer );
$this->pageStore = $pageStore;
}
/**
* Returns list of restrictions for specified page
*
* @param PageIdentity $page Must be local
* @param string $action Action that restrictions need to be checked for
* @return string[] Restriction levels needed to take the action. All levels are required. Note
* that restriction levels are normally user rights, but 'sysop' and 'autoconfirmed' are also
* allowed for backwards compatibility. These should be mapped to 'editprotected' and
* 'editsemiprotected' respectively. Returns an empty array if there are no restrictions set
* for this action (including for unrecognized actions).
*/
public function getRestrictions( PageIdentity $page, string $action ): array {
$page->assertWiki( PageIdentity::LOCAL );
$restrictions = $this->getAllRestrictions( $page );
return $restrictions[$action] ?? [];
}
/**
* Returns the restricted actions and their restrictions for the specified page
*
* @param PageIdentity $page Must be local
* @return string[][] Keys are actions, values are arrays as returned by
* RestrictionStore::getRestrictions(). Empty if no restrictions are in place.
*/
public function getAllRestrictions( PageIdentity $page ): array {
$page->assertWiki( PageIdentity::LOCAL );
if ( !$this->areRestrictionsLoaded( $page ) ) {
$this->loadRestrictions( $page );
}
return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['restrictions'] ?? [];
}
/**
* Get the expiry time for the restriction against a given action
*
* @param PageIdentity $page Must be local
* @param string $action
* @return ?string 14-char timestamp, or 'infinity' if the page is protected forever or not
* protected at all, or null if the action is not recognized. NOTE: This returns null for
* unrecognized actions, unlike Title::getRestrictionExpiry which returns false.
*/
public function getRestrictionExpiry( PageIdentity $page, string $action ): ?string {
$page->assertWiki( PageIdentity::LOCAL );
if ( !$this->areRestrictionsLoaded( $page ) ) {
$this->loadRestrictions( $page );
}
return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['expiry'][$action] ?? null;
}
/**
* Is this title subject to protection against creation?
*
* @param PageIdentity $page Must be local
* @return ?array Null if no restrictions. Otherwise an array with the following keys:
* - user: user id
* - expiry: 14-digit timestamp or 'infinity'
* - permission: string (pt_create_perm)
* - reason: string
* @internal Only to be called by Title::getTitleProtection. When that is discontinued, this
* will be too, in favor of getRestrictions( $page, 'create' ). If someone wants to know who
* protected it or the reason, there should be a method that exposes that for all restriction
* types.
*/
public function getCreateProtection( PageIdentity $page ): ?array {
$page->assertWiki( PageIdentity::LOCAL );
$protection = $this->getCreateProtectionInternal( $page );
// TODO: the remapping below probably need to be migrated into other method one day
if ( $protection ) {
if ( $protection['permission'] == 'sysop' ) {
$protection['permission'] = 'editprotected'; // B/C
}
if ( $protection['permission'] == 'autoconfirmed' ) {
$protection['permission'] = 'editsemiprotected'; // B/C
}
}
return $protection;
}
/**
* Remove any title creation protection due to page existing
*
* @param PageIdentity $page Must be local
* @internal Only to be called by WikiPage::onArticleCreate.
*/
public function deleteCreateProtection( PageIdentity $page ): void {
$page->assertWiki( PageIdentity::LOCAL );
$dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
$dbw->delete(
'protected_titles',
[ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ],
__METHOD__
);
$this->cache[CacheKeyHelper::getKeyForPage( $page )]['create_protection'] = null;
}
/**
* Is this page "semi-protected" - the *only* protection levels are listed in
* $wgSemiprotectedRestrictionLevels?
*
* @param PageIdentity $page Must be local
* @param string $action Action to check (default: edit)
* @return bool
*/
public function isSemiProtected( PageIdentity $page, string $action = 'edit' ): bool {
$page->assertWiki( PageIdentity::LOCAL );
$restrictions = $this->getRestrictions( $page, $action );
$semi = $this->options->get( 'SemiprotectedRestrictionLevels' );
if ( !$restrictions || !$semi ) {
// Not protected, or all protection is full protection
return false;
}
// Remap autoconfirmed to editsemiprotected for BC
foreach ( array_keys( $semi, 'editsemiprotected' ) as $key ) {
$semi[$key] = 'autoconfirmed';
}
foreach ( array_keys( $restrictions, 'editsemiprotected' ) as $key ) {
$restrictions[$key] = 'autoconfirmed';
}
return !array_diff( $restrictions, $semi );
}
/**
* Does the title correspond to a protected article?
*
* @param PageIdentity $page Must be local
* @param string $action The action the page is protected from, by default checks all actions.
* @return bool
*/
public function isProtected( PageIdentity $page, string $action = '' ): bool {
$page->assertWiki( PageIdentity::LOCAL );
// Special pages have inherent protection (TODO: remove after switch to ProperPageIdentity)
if ( $page->getNamespace() === NS_SPECIAL ) {
return true;
}
// Check regular protection levels
$applicableTypes = $this->listApplicableRestrictionTypes( $page );
if ( $action === '' ) {
foreach ( $applicableTypes as $type ) {
if ( $this->isProtected( $page, $type ) ) {
return true;
}
}
return false;
}
if ( !in_array( $action, $applicableTypes ) ) {
return false;
}
return (bool)array_diff(
array_intersect(
$this->getRestrictions( $page, $action ),
$this->options->get( 'RestrictionLevels' )
),
[ '' ]
);
}
/**
* Cascading protection: Return true if cascading restrictions apply to this page, false if not.
*
* @param PageIdentity $page Must be local
* @return bool If the page is subject to cascading restrictions.
*/
public function isCascadeProtected( PageIdentity $page ): bool {
$page->assertWiki( PageIdentity::LOCAL );
return $this->getCascadeProtectionSourcesInternal( $page, true );
}
/**
* Returns restriction types for the current page
*
* @param PageIdentity $page Must be local
* @return string[] Applicable restriction types
*/
public function listApplicableRestrictionTypes( PageIdentity $page ): array {
$page->assertWiki( PageIdentity::LOCAL );
if ( !$page->canExist() ) {
return [];
}
$types = $this->listAllRestrictionTypes( $page->exists() );
if ( $page->getNamespace() !== NS_FILE ) {
// Remove the upload restriction for non-file titles
$types = array_values( array_diff( $types, [ 'upload' ] ) );
}
if ( $this->hookContainer->isRegistered( 'TitleGetRestrictionTypes' ) ) {
$this->hookRunner->onTitleGetRestrictionTypes(
Title::castFromPageIdentity( $page ), $types );
}
return $types;
}
/**
* Get a filtered list of all restriction types supported by this wiki.
*
* @param bool $exists True to get all restriction types that apply to titles that do exist,
* false for all restriction types that apply to titles that do not exist
* @return string[]
*/
public function listAllRestrictionTypes( bool $exists = true ): array {
$types = $this->options->get( 'RestrictionTypes' );
if ( $exists ) {
// Remove the create restriction for existing titles
return array_values( array_diff( $types, [ 'create' ] ) );
}
// Only the create and upload restrictions apply to non-existing titles
return array_values( array_intersect( $types, [ 'create', 'upload' ] ) );
}
/**
* Load restrictions from page.page_restrictions and the page_restrictions table
*
* @param PageIdentity $page Must be local
* @param int $flags IDBAccessObject::READ_XXX constants (e.g., READ_LATEST to read from
* primary DB)
* @param string|null $oldRestrictions Restrictions in legacy format (page.page_restrictions).
* null means we don't know about any legacy restrictions and they need to be looked up.
* Example: "edit=autoconfirmed,sysop:move=sysop"
* @internal
*/
public function loadRestrictions(
PageIdentity $page, int $flags = IDBAccessObject::READ_NORMAL, ?string $oldRestrictions = null
): void {
$page->assertWiki( PageIdentity::LOCAL );
if ( !$page->canExist() ) {
return;
}
$readLatest = DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST );
if ( $this->areRestrictionsLoaded( $page ) && !$readLatest ) {
return;
}
$cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
$cacheEntry['restrictions'] = [];
// XXX Work around https://phabricator.wikimedia.org/T287575
if ( $readLatest ) {
$page = $this->pageStore->getPageByReference( $page, $flags ) ?? $page;
}
$id = $page->getId();
if ( $id ) {
$fname = __METHOD__;
$loadRestrictionsFromDb = static function ( IDatabase $dbr ) use ( $fname, $id ) {
return iterator_to_array(
$dbr->select(
'page_restrictions',
[ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
[ 'pr_page' => $id ],
$fname
)
);
};
if ( $readLatest ) {
$dbr = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
$rows = $loadRestrictionsFromDb( $dbr );
} else {
$this->linkCache->addLinkObj( $page );
$latestRev = $this->linkCache->getGoodLinkFieldObj( $page, 'revision' );
$rows = $this->wanCache->getWithSetCallback(
// Page protections always leave a new null revision
$this->wanCache->makeKey( 'page-restrictions', 'v1', $id, $latestRev ),
$this->wanCache::TTL_DAY,
function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) {
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
if ( $this->loadBalancer->hasOrMadeRecentPrimaryChanges() ) {
// TODO: cleanup Title cache and caller assumption mess in general
$ttl = WANObjectCache::TTL_UNCACHEABLE;
}
return $loadRestrictionsFromDb( $dbr );
}
);
}
$this->loadRestrictionsFromRows( $page, $rows, $oldRestrictions );
} else {
$titleProtection = $this->getCreateProtectionInternal( $page );
if ( $titleProtection ) {
$now = wfTimestampNow();
$expiry = $titleProtection['expiry'];
if ( !$expiry || $expiry > $now ) {
// Apply the restrictions
$cacheEntry['expiry']['create'] = $expiry ?: null;
$cacheEntry['restrictions']['create'] =
explode( ',', trim( $titleProtection['permission'] ) );
} else {
// Get rid of the old restrictions
$cacheEntry['create_protection'] = null;
}
} else {
$cacheEntry['expiry']['create'] = 'infinity';
}
}
}
/**
* Compiles list of active page restrictions for this existing page.
* Public for usage by LiquidThreads.
*
* @param PageIdentity $page Must be local
* @param stdClass[] $rows Array of db result objects
* @param string|null $oldRestrictions Restrictions in legacy format (page.page_restrictions).
* null means we don't know about any legacy restrictions and they need to be looked up.
* Example: "edit=autoconfirmed,sysop:move=sysop"
*/
public function loadRestrictionsFromRows(
PageIdentity $page, array $rows, ?string $oldRestrictions = null
): void {
$page->assertWiki( PageIdentity::LOCAL );
$cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
$restrictionTypes = $this->listApplicableRestrictionTypes( $page );
foreach ( $restrictionTypes as $type ) {
$cacheEntry['restrictions'][$type] = [];
$cacheEntry['expiry'][$type] = 'infinity';
}
$cacheEntry['cascade'] = false;
// Backwards-compatibility: also load the restrictions from the page record (old format).
// Don't include in test coverage, we're planning to drop support.
// @codeCoverageIgnoreStart
$cacheEntry['oldRestrictions'] = $oldRestrictions ?? $cacheEntry['oldRestrictions'] ?? null;
if ( $cacheEntry['oldRestrictions'] === null ) {
$this->linkCache->addLinkObj( $page );
$cachedOldRestrictions = $this->linkCache->getGoodLinkFieldObj( $page, 'restrictions' );
if ( $cachedOldRestrictions !== null ) {
$cacheEntry['oldRestrictions'] = $cachedOldRestrictions;
}
}
if ( $cacheEntry['oldRestrictions'] ) {
$cacheEntry['restrictions'] =
$this->convertOldRestrictions( $cacheEntry['oldRestrictions'] );
}
// @codeCoverageIgnoreEnd
if ( !$rows ) {
return;
}
// New restriction format -- load second to make them override old-style restrictions.
$now = wfTimestampNow();
// Cycle through all the restrictions.
foreach ( $rows as $row ) {
// Don't take care of restrictions types that aren't allowed
if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
continue;
}
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
$expiry = $dbr->decodeExpiry( $row->pr_expiry );
// Only apply the restrictions if they haven't expired!
// XXX Why would !$expiry ever be true? It should always be either 'infinity' or a
// string consisting of 14 digits. Likewise for the ?: below.
if ( !$expiry || $expiry > $now ) {
$cacheEntry['expiry'][$row->pr_type] = $expiry ?: null;
$cacheEntry['restrictions'][$row->pr_type]
= explode( ',', trim( $row->pr_level ) );
if ( $row->pr_cascade ) {
$cacheEntry['cascade'] = true;
}
}
}
}
/**
* Given a string formatted like the legacy page.page_restrictions field, return an array of
* restrictions in the format returned by getAllRestrictions().
*
* @param string $oldRestrictions Restrictions in legacy format (page.page_restrictions).
* Example: "edit=autoconfirmed,sysop:move=sysop"
* @return array As returned by getAllRestrictions()
* @codeCoverageIgnore We're planning to drop support for this
*/
private function convertOldRestrictions( string $oldRestrictions ): array {
$ret = [];
foreach ( explode( ':', trim( $oldRestrictions ) ) as $restrict ) {
$restrictionPair = explode( '=', trim( $restrict ) );
if ( count( $restrictionPair ) == 1 ) {
// old old format should be treated as edit/move restriction
$ret['edit'] = explode( ',', trim( $restrictionPair[0] ) );
$ret['move'] = explode( ',', trim( $restrictionPair[0] ) );
} else {
$restriction = trim( $restrictionPair[1] );
if ( $restriction != '' ) { // some old entries are empty
$ret[$restrictionPair[0]] = explode( ',', $restriction );
}
}
}
return $ret;
}
/**
* Fetch title protection settings
*
* To work correctly, $this->loadRestrictions() needs to have access to the actual protections
* in the database without munging 'sysop' => 'editprotected' and 'autoconfirmed' =>
* 'editsemiprotected'.
*
* @param PageIdentity $page Must be local
* @return ?array Same format as getCreateProtection().
*/
private function getCreateProtectionInternal( PageIdentity $page ): ?array {
// Can't protect pages in special namespaces
if ( !$page->canExist() ) {
return null;
}
// Can't apply this type of protection to pages that exist.
if ( $page->exists() ) {
return null;
}
$cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
if ( !$cacheEntry || !array_key_exists( 'create_protection', $cacheEntry ) ) {
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
$commentQuery = $this->commentStore->getJoin( 'pt_reason' );
$row = $dbr->selectRow(
[ 'protected_titles' ] + $commentQuery['tables'],
[ 'pt_user', 'pt_expiry', 'pt_create_perm' ] + $commentQuery['fields'],
[ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ],
__METHOD__,
[],
$commentQuery['joins']
);
if ( $row ) {
$cacheEntry['create_protection'] = [
'user' => $row->pt_user,
'expiry' => $dbr->decodeExpiry( $row->pt_expiry ),
'permission' => $row->pt_create_perm,
'reason' => $this->commentStore->getComment( 'pt_reason', $row )->text,
];
} else {
$cacheEntry['create_protection'] = null;
}
}
return $cacheEntry['create_protection'];
}
/**
* Cascading protection: Get the source of any cascading restrictions on this page.
*
* @param PageIdentity $page Must be local
* @return array[] Two elements: First is an array of PageIdentity objects of the pages from
* which cascading restrictions have come, which may be empty. Second is an array like that
* returned by getAllRestrictions(). NOTE: The first element of the return is always an
* array, unlike Title::getCascadeProtectionSources where the first element is false if there
* are no sources.
*/
public function getCascadeProtectionSources( PageIdentity $page ): array {
$page->assertWiki( PageIdentity::LOCAL );
return $this->getCascadeProtectionSourcesInternal( $page, false );
}
/**
* Cascading protection: Get the source of any cascading restrictions on this page.
*
* @param PageIdentity $page Must be local
* @param bool $shortCircuit If true, just return true or false instead of the actual lists.
* @return array|bool If $shortCircuit is true, return true if there is some cascading
* protection and false otherwise. Otherwise, same as getCascadeProtectionSources().
*/
private function getCascadeProtectionSourcesInternal(
PageIdentity $page, bool $shortCircuit = false
) {
$cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
if ( !$shortCircuit && isset( $cacheEntry['cascade_sources'] ) ) {
return $cacheEntry['cascade_sources'];
} elseif ( $shortCircuit && isset( $cacheEntry['has_cascading'] ) ) {
return $cacheEntry['has_cascading'];
}
if ( $page->getNamespace() === NS_FILE ) {
// Files transclusion may receive cascading protection in the future
// see https://phabricator.wikimedia.org/T241453
$tables = [ 'imagelinks', 'page_restrictions' ];
$where_clauses = [
'il_to' => $page->getDBkey(),
'il_from=pr_page',
'pr_cascade' => 1
];
} else {
$tables = [ 'templatelinks', 'page_restrictions' ];
$where_clauses = [
'tl_namespace' => $page->getNamespace(),
'tl_title' => $page->getDBkey(),
'tl_from=pr_page',
'pr_cascade' => 1
];
}
if ( $shortCircuit ) {
$cols = [ 'pr_expiry' ];
} else {
$cols = [ 'pr_page', 'page_namespace', 'page_title',
'pr_expiry', 'pr_type', 'pr_level' ];
$where_clauses[] = 'page_id=pr_page';
$tables[] = 'page';
}
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
$sources = [];
$pageRestrictions = [];
$now = wfTimestampNow();
foreach ( $res as $row ) {
$expiry = $dbr->decodeExpiry( $row->pr_expiry );
if ( $expiry > $now ) {
if ( $shortCircuit ) {
$cacheEntry['has_cascading'] = true;
return true;
}
$sources[$row->pr_page] = new PageIdentityValue( $row->pr_page,
$row->page_namespace, $row->page_title, PageIdentity::LOCAL );
// Add groups needed for each restriction type if its not already there
// Make sure this restriction type still exists
if ( !isset( $pageRestrictions[$row->pr_type] ) ) {
$pageRestrictions[$row->pr_type] = [];
}
if ( !in_array( $row->pr_level, $pageRestrictions[$row->pr_type] ) ) {
$pageRestrictions[$row->pr_type][] = $row->pr_level;
}
}
}
$cacheEntry['has_cascading'] = (bool)$sources;
if ( $shortCircuit ) {
return false;
}
$cacheEntry['cascade_sources'] = [ $sources, $pageRestrictions ];
return [ $sources, $pageRestrictions ];
}
/**
* @param PageIdentity $page Must be local
* @return bool Whether or not the page's restrictions have already been loaded from the
* database
*/
public function areRestrictionsLoaded( PageIdentity $page ): bool {
$page->assertWiki( PageIdentity::LOCAL );
return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['restrictions'] );
}
/**
* Determines whether cascading protection sources have already been loaded from the database.
*
* @param PageIdentity $page Must be local
* @return bool
*/
public function areCascadeProtectionSourcesLoaded( PageIdentity $page ): bool {
$page->assertWiki( PageIdentity::LOCAL );
return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade_sources'] );
}
/**
* Checks if restrictions are cascading for the current page
*
* @param PageIdentity $page Must be local
* @return bool
*/
public function areRestrictionsCascading( PageIdentity $page ): bool {
$page->assertWiki( PageIdentity::LOCAL );
if ( !$this->areRestrictionsLoaded( $page ) ) {
$this->loadRestrictions( $page );
}
return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade'] ?? false;
}
/**
* Flush the protection cache in this object and force reload from the database. This is used
* when updating protection from WikiPage::doUpdateRestrictions().
*
* @param PageIdentity $page Must be local
* @internal
*/
public function flushRestrictions( PageIdentity $page ): void {
$page->assertWiki( PageIdentity::LOCAL );
unset( $this->cache[CacheKeyHelper::getKeyForPage( $page )] );
}
/**
* Register legacy restrictions from page.page_restrictions. This is nice to do if you have a
* page row handy anyway, so we don't have to look them up separately later.
*
* @param PageIdentity $page Must be local
* @param string $oldRestrictions Restrictions in legacy format (page.page_restrictions).
* Example: "edit=autoconfirmed,sysop:move=sysop"
* @internal
* @codeCoverageIgnore We're planning to drop support for this
*/
public function registerOldRestrictions( PageIdentity $page, string $oldRestrictions ): void {
$page->assertWiki( PageIdentity::LOCAL );
$this->cache[CacheKeyHelper::getKeyForPage( $page )]['oldRestrictions'] =
$oldRestrictions;
}
}