New RestrictionStore service
This allows checking restrictions without a dependency on Title, based only on a PageIdentity. Additional fixes along the way: * Correctly return false instead of 'infinity' for getRestrictionExpiry( 'create' ) on an existing page * Correctly handle non-special pages that can't exist (like media pages) in listApplicableRestrictionTypes() (return empty array instead of 'create') * Improve readability of isProtected() The expectation change in TitleTest::testIsProtected() is because the test was formerly broken, since it set mRestrictions without setting mRestrictionsLoaded. (Which illustrates how this approach to testing is essentially broken.) Co-authored-by: Vedmaka <god.vedmaka@gmail.com> Bug: T218395 Change-Id: Ia73ea587586cb69eb53265b2f8f7a296a2573dd0
This commit is contained in:
parent
d55f361a5b
commit
cf818256fb
10 changed files with 2261 additions and 469 deletions
|
|
@ -640,6 +640,30 @@ because of Phabricator reports.
|
|||
getUserPermissionsErrorsExpensive instead.
|
||||
* Parser::mUser public access, Parser::getUser and ParserOptions::getUser were
|
||||
hard deprecated.
|
||||
* The following methods in the Title class have been deprecated in favor of the
|
||||
corresponding methods in the new RestrictionStore service (with different
|
||||
names where indicated):
|
||||
- areCascadeProtectionSourcesLoaded()
|
||||
- areRestrictionsCascading()
|
||||
- areRestrictionsLoaded()
|
||||
- getAllRestrictions()
|
||||
- getCascadeProtectionSources()
|
||||
- getFilteredRestrictionTypes()
|
||||
-> listAllRestrictionTypes()
|
||||
- getRestrictionExpiry()
|
||||
- getRestrictionTypes()
|
||||
-> listApplicableRestrictionTypes()
|
||||
- getRestrictions()
|
||||
- isCascadeProtected()
|
||||
- isProtected()
|
||||
- isSemiProtected()
|
||||
- loadRestrictionsFromRows()
|
||||
* The following Title methods have been deprecated with no direct public
|
||||
replacement:
|
||||
- deleteTitleProtection()
|
||||
- getTitleProtection()
|
||||
- flushRestrictions()
|
||||
- loadRestrictions()
|
||||
* …
|
||||
|
||||
=== Other changes in 1.37 ===
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ use MediaWiki\Page\WikiPageFactory;
|
|||
use MediaWiki\Parser\ParserCacheFactory;
|
||||
use MediaWiki\Permissions\GroupPermissionsLookup;
|
||||
use MediaWiki\Permissions\PermissionManager;
|
||||
use MediaWiki\Permissions\RestrictionStore;
|
||||
use MediaWiki\Preferences\PreferencesFactory;
|
||||
use MediaWiki\Revision\ContributionsLookup;
|
||||
use MediaWiki\Revision\RevisionFactory;
|
||||
|
|
@ -1363,6 +1364,14 @@ class MediaWikiServices extends ServiceContainer {
|
|||
return $this->getService( 'ResourceLoader' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.37
|
||||
* @return RestrictionStore
|
||||
*/
|
||||
public function getRestrictionStore(): RestrictionStore {
|
||||
return $this->getService( 'RestrictionStore' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.36
|
||||
* @return RevertedTagUpdateManager
|
||||
|
|
|
|||
751
includes/Permissions/RestrictionStore.php
Normal file
751
includes/Permissions/RestrictionStore.php
Normal file
|
|
@ -0,0 +1,751 @@
|
|||
<?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->hasOrMadeRecentMasterChanges() ) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -100,6 +100,7 @@ use MediaWiki\Page\WikiPageFactory;
|
|||
use MediaWiki\Parser\ParserCacheFactory;
|
||||
use MediaWiki\Permissions\GroupPermissionsLookup;
|
||||
use MediaWiki\Permissions\PermissionManager;
|
||||
use MediaWiki\Permissions\RestrictionStore;
|
||||
use MediaWiki\Preferences\DefaultPreferencesFactory;
|
||||
use MediaWiki\Preferences\PreferencesFactory;
|
||||
use MediaWiki\Revision\ContributionsLookup;
|
||||
|
|
@ -1324,6 +1325,20 @@ return [
|
|||
return $rl;
|
||||
},
|
||||
|
||||
'RestrictionStore' => static function ( MediaWikiServices $services ): RestrictionStore {
|
||||
return new RestrictionStore(
|
||||
new ServiceOptions(
|
||||
RestrictionStore::CONSTRUCTOR_OPTIONS, $services->getMainConfig()
|
||||
),
|
||||
$services->getMainWANObjectCache(),
|
||||
$services->getDBLoadBalancer(),
|
||||
$services->getLinkCache(),
|
||||
$services->getCommentStore(),
|
||||
$services->getHookContainer(),
|
||||
$services->getPageStore()
|
||||
);
|
||||
},
|
||||
|
||||
'RevertedTagUpdateManager' => static function ( MediaWikiServices $services ): RevertedTagUpdateManager {
|
||||
$editResultCache = new EditResultCache(
|
||||
$services->getMainObjectStash(),
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ use MediaWiki\Page\PageStoreRecord;
|
|||
use MediaWiki\Page\ProperPageIdentity;
|
||||
use Wikimedia\Assert\Assert;
|
||||
use Wikimedia\Assert\PreconditionException;
|
||||
use Wikimedia\Rdbms\Database;
|
||||
use Wikimedia\Rdbms\IDatabase;
|
||||
|
||||
/**
|
||||
|
|
@ -47,6 +46,7 @@ use Wikimedia\Rdbms\IDatabase;
|
|||
* and does not rely on global state or the database.
|
||||
*/
|
||||
class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
||||
use DeprecationHelper;
|
||||
use WikiAwareEntityTrait;
|
||||
|
||||
/** @var MapCacheLRU|null */
|
||||
|
|
@ -149,35 +149,6 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
/** @var int Estimated number of revisions; null of not loaded */
|
||||
private $mEstimateRevisions;
|
||||
|
||||
/** @var array Array of groups allowed to edit this article */
|
||||
public $mRestrictions = [];
|
||||
|
||||
/**
|
||||
* @var string|bool Comma-separated set of permission keys
|
||||
* indicating who can move or edit the page from the page table, (pre 1.10) rows.
|
||||
* Edit and move sections are separated by a colon
|
||||
* Example: "edit=autoconfirmed,sysop:move=sysop"
|
||||
*/
|
||||
protected $mOldRestrictions = false;
|
||||
|
||||
/** @var bool Cascade restrictions on this page to included templates and images? */
|
||||
public $mCascadeRestriction;
|
||||
|
||||
/** Caching the results of getCascadeProtectionSources */
|
||||
public $mCascadingRestrictions;
|
||||
|
||||
/** @var array When do the restrictions on this page expire? */
|
||||
protected $mRestrictionsExpiry = [];
|
||||
|
||||
/** @var bool Are cascading restrictions in effect on this page? */
|
||||
protected $mHasCascadingRestrictions;
|
||||
|
||||
/** @var array Where are the cascading restrictions coming from on this page? */
|
||||
public $mCascadeSources;
|
||||
|
||||
/** @var bool Boolean for initialisation on demand */
|
||||
public $mRestrictionsLoaded = false;
|
||||
|
||||
/**
|
||||
* Text form including namespace/interwiki, initialised on demand
|
||||
*
|
||||
|
|
@ -188,9 +159,6 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
*/
|
||||
public $prefixedText = null;
|
||||
|
||||
/** @var mixed Cached value for getTitleProtection (create protection) */
|
||||
public $mTitleProtection;
|
||||
|
||||
/**
|
||||
* @var int Namespace index when there is no namespace. Don't change the
|
||||
* following default, NS_MAIN is hardcoded in several places. See T2696.
|
||||
|
|
@ -644,7 +612,10 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
$this->mDbPageLanguage = (string)$row->page_lang;
|
||||
}
|
||||
if ( isset( $row->page_restrictions ) ) {
|
||||
$this->mOldRestrictions = $row->page_restrictions;
|
||||
// If we have them handy, save them so we don't need to look them up later
|
||||
MediaWikiServices::getInstance()->getRestrictionStore()
|
||||
->registerOldRestrictions( $this, $row->page_restrictions );
|
||||
|
||||
}
|
||||
} else { // page not found
|
||||
$this->mArticleID = 0;
|
||||
|
|
@ -2470,195 +2441,84 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
|
||||
/**
|
||||
* Get a filtered list of all restriction types supported by this wiki.
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::listAllRestrictionTypes instead
|
||||
*
|
||||
* @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 array
|
||||
*/
|
||||
public static function getFilteredRestrictionTypes( $exists = true ) {
|
||||
global $wgRestrictionTypes;
|
||||
$types = $wgRestrictionTypes;
|
||||
if ( $exists ) {
|
||||
# Remove the create restriction for existing titles
|
||||
$types = array_diff( $types, [ 'create' ] );
|
||||
} else {
|
||||
# Only the create and upload restrictions apply to non-existing titles
|
||||
$types = array_intersect( $types, [ 'create', 'upload' ] );
|
||||
}
|
||||
return $types;
|
||||
return MediaWikiServices::getInstance()
|
||||
->getRestrictionStore()
|
||||
->listAllRestrictionTypes( $exists );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns restriction types for the current Title
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::listApplicableRestrictionTypes instead
|
||||
*
|
||||
* @return array Applicable restriction types
|
||||
*/
|
||||
public function getRestrictionTypes() {
|
||||
if ( $this->isSpecialPage() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$types = self::getFilteredRestrictionTypes( $this->exists() );
|
||||
|
||||
if ( $this->mNamespace !== NS_FILE ) {
|
||||
# Remove the upload restriction for non-file titles
|
||||
$types = array_diff( $types, [ 'upload' ] );
|
||||
}
|
||||
|
||||
Hooks::runner()->onTitleGetRestrictionTypes( $this, $types );
|
||||
|
||||
wfDebug( __METHOD__ . ': applicable restrictions to [[' .
|
||||
$this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}" );
|
||||
|
||||
return $types;
|
||||
return MediaWikiServices::getInstance()
|
||||
->getRestrictionStore()
|
||||
->listApplicableRestrictionTypes( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this title subject to title protection?
|
||||
* Title protection is the one applied against creation of such title.
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::getRestrictions() instead
|
||||
*
|
||||
* @return array|bool An associative array representing any existent title
|
||||
* protection, or false if there's none.
|
||||
*/
|
||||
public function getTitleProtection() {
|
||||
$protection = $this->getTitleProtectionInternal();
|
||||
if ( $protection ) {
|
||||
if ( $protection['permission'] == 'sysop' ) {
|
||||
$protection['permission'] = 'editprotected'; // B/C
|
||||
}
|
||||
if ( $protection['permission'] == 'autoconfirmed' ) {
|
||||
$protection['permission'] = 'editsemiprotected'; // B/C
|
||||
}
|
||||
}
|
||||
return $protection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'. Other
|
||||
* callers probably want $this->getTitleProtection() instead.
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
protected function getTitleProtectionInternal() {
|
||||
// Can't protect pages in special namespaces
|
||||
if ( $this->mNamespace < 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Can't protect pages that exist.
|
||||
if ( $this->exists() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->mTitleProtection === null ) {
|
||||
$dbr = wfGetDB( DB_REPLICA );
|
||||
$commentStore = CommentStore::getStore();
|
||||
$commentQuery = $commentStore->getJoin( 'pt_reason' );
|
||||
$res = $dbr->select(
|
||||
[ 'protected_titles' ] + $commentQuery['tables'],
|
||||
[
|
||||
'user' => 'pt_user',
|
||||
'expiry' => 'pt_expiry',
|
||||
'permission' => 'pt_create_perm'
|
||||
] + $commentQuery['fields'],
|
||||
[ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
|
||||
__METHOD__,
|
||||
[],
|
||||
$commentQuery['joins']
|
||||
);
|
||||
|
||||
// fetchRow returns false if there are no rows.
|
||||
$row = $dbr->fetchRow( $res );
|
||||
if ( $row ) {
|
||||
$this->mTitleProtection = [
|
||||
'user' => $row['user'],
|
||||
'expiry' => $dbr->decodeExpiry( $row['expiry'] ),
|
||||
'permission' => $row['permission'],
|
||||
'reason' => $commentStore->getComment( 'pt_reason', $row )->text,
|
||||
];
|
||||
} else {
|
||||
$this->mTitleProtection = false;
|
||||
}
|
||||
}
|
||||
return $this->mTitleProtection;
|
||||
return MediaWikiServices::getInstance()->getRestrictionStore()->getCreateProtection( $this )
|
||||
?: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any title protection due to page existing
|
||||
*
|
||||
* @deprecated since 1.37, do not use (this is only for WikiPage::onArticleCreate)
|
||||
*/
|
||||
public function deleteTitleProtection() {
|
||||
$dbw = wfGetDB( DB_PRIMARY );
|
||||
|
||||
$dbw->delete(
|
||||
'protected_titles',
|
||||
[ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
|
||||
__METHOD__
|
||||
);
|
||||
$this->mTitleProtection = false;
|
||||
MediaWikiServices::getInstance()->getRestrictionStore()->deleteCreateProtection( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this page "semi-protected" - the *only* protection levels are listed
|
||||
* in $wgSemiprotectedRestrictionLevels?
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::isSemiProtected instead
|
||||
*
|
||||
* @param string $action Action to check (default: edit)
|
||||
* @return bool
|
||||
*/
|
||||
public function isSemiProtected( $action = 'edit' ) {
|
||||
global $wgSemiprotectedRestrictionLevels;
|
||||
|
||||
$restrictions = $this->getRestrictions( $action );
|
||||
$semi = $wgSemiprotectedRestrictionLevels;
|
||||
if ( !$restrictions || !$semi ) {
|
||||
// Not protected, or all protection is full protection
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remap autoconfirmed to editsemiprotected for BC
|
||||
foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
|
||||
$semi[$key] = 'editsemiprotected';
|
||||
}
|
||||
foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
|
||||
$restrictions[$key] = 'editsemiprotected';
|
||||
}
|
||||
|
||||
return !array_diff( $restrictions, $semi );
|
||||
return MediaWikiServices::getInstance()->getRestrictionStore()->isSemiProtected(
|
||||
$this, $action
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the title correspond to a protected article?
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::isProtected instead
|
||||
*
|
||||
* @param string $action The action the page is protected from,
|
||||
* by default checks all actions.
|
||||
* @return bool
|
||||
*/
|
||||
public function isProtected( $action = '' ) {
|
||||
global $wgRestrictionLevels;
|
||||
|
||||
$restrictionTypes = $this->getRestrictionTypes();
|
||||
|
||||
# Special pages have inherent protection
|
||||
if ( $this->isSpecialPage() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
# Check regular protection levels
|
||||
foreach ( $restrictionTypes as $type ) {
|
||||
if ( $action == $type || $action == '' ) {
|
||||
$r = $this->getRestrictions( $type );
|
||||
foreach ( $wgRestrictionLevels as $level ) {
|
||||
if ( in_array( $level, $r ) && $level != '' ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return MediaWikiServices::getInstance()->getRestrictionStore()->isProtected(
|
||||
$this, $action
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2688,29 +2548,33 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
/**
|
||||
* Cascading protection: Return true if cascading restrictions apply to this page, false if not.
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::isCascadeProtected instead
|
||||
*
|
||||
* @return bool If the page is subject to cascading restrictions.
|
||||
*/
|
||||
public function isCascadeProtected() {
|
||||
list( $isCascadeProtected, ) = $this->getCascadeProtectionSources( false );
|
||||
return $isCascadeProtected;
|
||||
return MediaWikiServices::getInstance()->getRestrictionStore()->isCascadeProtected( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether cascading protection sources have already been loaded from
|
||||
* the database.
|
||||
*
|
||||
* @param bool $getPages True to check if the pages are loaded, or false to check
|
||||
* if the status is loaded.
|
||||
* @return bool Whether or not the specified information has been loaded
|
||||
* @deprecated since 1.37, use RestrictionStore::areCascadeProtectionSourcesLoaded instead
|
||||
*
|
||||
* @return bool
|
||||
* @since 1.23
|
||||
*/
|
||||
public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
|
||||
return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
|
||||
public function areCascadeProtectionSourcesLoaded() {
|
||||
return MediaWikiServices::getInstance()->getRestrictionStore()
|
||||
->areCascadeProtectionSourcesLoaded( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cascading protection: Get the source of any cascading restrictions on this page.
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::getCascadeProtectionSources instead
|
||||
*
|
||||
* @param bool $getPages Whether or not to retrieve the actual pages
|
||||
* that the restrictions have come from and the actual restrictions
|
||||
* themselves.
|
||||
|
|
@ -2722,98 +2586,39 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
* false.
|
||||
*/
|
||||
public function getCascadeProtectionSources( $getPages = true ) {
|
||||
$pagerestrictions = [];
|
||||
|
||||
if ( $this->mCascadeSources !== null && $getPages ) {
|
||||
return [ $this->mCascadeSources, $this->mCascadingRestrictions ];
|
||||
} elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
|
||||
return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
|
||||
$restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore();
|
||||
if ( !$getPages ) {
|
||||
return [ $restrictionStore->isCascadeProtected( $this ), [] ];
|
||||
}
|
||||
|
||||
$dbr = wfGetDB( DB_REPLICA );
|
||||
|
||||
if ( $this->mNamespace === NS_FILE ) {
|
||||
$tables = [ 'imagelinks', 'page_restrictions' ];
|
||||
$where_clauses = [
|
||||
'il_to' => $this->mDbkeyform,
|
||||
'il_from=pr_page',
|
||||
'pr_cascade' => 1
|
||||
];
|
||||
} else {
|
||||
$tables = [ 'templatelinks', 'page_restrictions' ];
|
||||
$where_clauses = [
|
||||
'tl_namespace' => $this->mNamespace,
|
||||
'tl_title' => $this->mDbkeyform,
|
||||
'tl_from=pr_page',
|
||||
'pr_cascade' => 1
|
||||
];
|
||||
$ret = $restrictionStore->getCascadeProtectionSources( $this );
|
||||
$ret[0] = array_map( 'Title::castFromPageIdentity', $ret[0] );
|
||||
if ( !$ret[0] ) {
|
||||
$ret[0] = false;
|
||||
}
|
||||
|
||||
if ( $getPages ) {
|
||||
$cols = [ 'pr_page', 'page_namespace', 'page_title',
|
||||
'pr_expiry', 'pr_type', 'pr_level' ];
|
||||
$where_clauses[] = 'page_id=pr_page';
|
||||
$tables[] = 'page';
|
||||
} else {
|
||||
$cols = [ 'pr_expiry' ];
|
||||
}
|
||||
|
||||
$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
|
||||
|
||||
$sources = $getPages ? [] : false;
|
||||
$now = wfTimestampNow();
|
||||
|
||||
foreach ( $res as $row ) {
|
||||
$expiry = $dbr->decodeExpiry( $row->pr_expiry );
|
||||
if ( $expiry > $now ) {
|
||||
if ( $getPages ) {
|
||||
$page_id = $row->pr_page;
|
||||
$page_ns = $row->page_namespace;
|
||||
$page_title = $row->page_title;
|
||||
$sources[$page_id] = self::makeTitle( $page_ns, $page_title );
|
||||
# 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 (
|
||||
isset( $pagerestrictions[$row->pr_type] )
|
||||
&& !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
|
||||
) {
|
||||
$pagerestrictions[$row->pr_type][] = $row->pr_level;
|
||||
}
|
||||
} else {
|
||||
$sources = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $getPages ) {
|
||||
$this->mCascadeSources = $sources;
|
||||
$this->mCascadingRestrictions = $pagerestrictions;
|
||||
} else {
|
||||
$this->mHasCascadingRestrictions = $sources;
|
||||
}
|
||||
|
||||
return [ $sources, $pagerestrictions ];
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for mRestrictionsLoaded
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::areRestrictionsLoaded instead
|
||||
*
|
||||
* @return bool Whether or not the page's restrictions have already been
|
||||
* loaded from the database
|
||||
* @since 1.23
|
||||
*/
|
||||
public function areRestrictionsLoaded() {
|
||||
return $this->mRestrictionsLoaded;
|
||||
return MediaWikiServices::getInstance()
|
||||
->getRestrictionStore()
|
||||
->areRestrictionsLoaded( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor/initialisation for mRestrictions
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::getRestrictions instead
|
||||
*
|
||||
* @param string $action Action that permission needs to be checked for
|
||||
* @return array Restriction levels needed to take the action. All levels are
|
||||
* required. Note that restriction levels are normally user rights, but 'sysop'
|
||||
|
|
@ -2821,51 +2626,48 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
* be mapped to 'editprotected' and 'editsemiprotected' respectively.
|
||||
*/
|
||||
public function getRestrictions( $action ) {
|
||||
if ( !$this->mRestrictionsLoaded ) {
|
||||
$this->loadRestrictions();
|
||||
}
|
||||
return $this->mRestrictions[$action] ?? [];
|
||||
return MediaWikiServices::getInstance()->getRestrictionStore()->getRestrictions( $this, $action );
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor/initialisation for mRestrictions
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::getAllRestrictions instead
|
||||
*
|
||||
* @return array Keys are actions, values are arrays as returned by
|
||||
* Title::getRestrictions()
|
||||
* @since 1.23
|
||||
*/
|
||||
public function getAllRestrictions() {
|
||||
if ( !$this->mRestrictionsLoaded ) {
|
||||
$this->loadRestrictions();
|
||||
}
|
||||
return $this->mRestrictions;
|
||||
return MediaWikiServices::getInstance()->getRestrictionStore()->getAllRestrictions( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expiry time for the restriction against a given action
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::getRestrictionExpiry instead
|
||||
*
|
||||
* @param string $action
|
||||
* @return string|bool 14-char timestamp, or 'infinity' if the page is protected forever
|
||||
* or not protected at all, or false if the action is not recognised.
|
||||
*/
|
||||
public function getRestrictionExpiry( $action ) {
|
||||
if ( !$this->mRestrictionsLoaded ) {
|
||||
$this->loadRestrictions();
|
||||
}
|
||||
return $this->mRestrictionsExpiry[$action] ?? false;
|
||||
return MediaWikiServices::getInstance()->getRestrictionStore()->getRestrictionExpiry(
|
||||
$this, $action
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cascading restrictions for the current article
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::areRestrictionsCascading instead
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function areRestrictionsCascading() {
|
||||
if ( !$this->mRestrictionsLoaded ) {
|
||||
$this->loadRestrictions();
|
||||
}
|
||||
|
||||
return $this->mCascadeRestriction;
|
||||
return MediaWikiServices::getInstance()
|
||||
->getRestrictionStore()
|
||||
->areRestrictionsCascading( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2873,6 +2675,8 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
* and page_restrictions table for this existing page.
|
||||
* Public for usage by LiquidThreads.
|
||||
*
|
||||
* @deprecated since 1.37, use RestrictionStore::loadRestrictionsFromRows instead
|
||||
*
|
||||
* @param stdClass[] $rows Array of db result objects
|
||||
* @param string|null $oldFashionedRestrictions Comma-separated set of permission keys
|
||||
* indicating who can move or edit the page from the page table, (pre 1.10) rows.
|
||||
|
|
@ -2880,76 +2684,16 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
* Example: "edit=autoconfirmed,sysop:move=sysop"
|
||||
*/
|
||||
public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
|
||||
// This function will only read rows from a table that we migrated away
|
||||
// from before adding READ_LATEST support to loadRestrictions, so we
|
||||
// don't need to support reading from DB_PRIMARY here.
|
||||
$dbr = wfGetDB( DB_REPLICA );
|
||||
|
||||
$restrictionTypes = $this->getRestrictionTypes();
|
||||
|
||||
foreach ( $restrictionTypes as $type ) {
|
||||
$this->mRestrictions[$type] = [];
|
||||
$this->mRestrictionsExpiry[$type] = 'infinity';
|
||||
}
|
||||
|
||||
$this->mCascadeRestriction = false;
|
||||
|
||||
# Backwards-compatibility: also load the restrictions from the page record (old format).
|
||||
if ( $oldFashionedRestrictions !== null ) {
|
||||
$this->mOldRestrictions = $oldFashionedRestrictions;
|
||||
}
|
||||
|
||||
if ( $this->mOldRestrictions === false ) {
|
||||
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
|
||||
$linkCache->addLinkObj( $this ); # in case we already had an article ID
|
||||
$this->mOldRestrictions = $linkCache->getGoodLinkFieldObj( $this, 'restrictions' );
|
||||
}
|
||||
|
||||
if ( $this->mOldRestrictions != '' ) {
|
||||
foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
|
||||
$temp = explode( '=', trim( $restrict ) );
|
||||
if ( count( $temp ) == 1 ) {
|
||||
// old old format should be treated as edit/move restriction
|
||||
$this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
|
||||
$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
|
||||
} else {
|
||||
$restriction = trim( $temp[1] );
|
||||
if ( $restriction != '' ) { // some old entries are empty
|
||||
$this->mRestrictions[$temp[0]] = explode( ',', $restriction );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $rows ) ) {
|
||||
# Current system - load second to make them override.
|
||||
$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;
|
||||
}
|
||||
|
||||
$expiry = $dbr->decodeExpiry( $row->pr_expiry );
|
||||
|
||||
// Only apply the restrictions if they haven't expired!
|
||||
if ( !$expiry || $expiry > $now ) {
|
||||
$this->mRestrictionsExpiry[$row->pr_type] = $expiry;
|
||||
$this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
|
||||
|
||||
$this->mCascadeRestriction = $this->mCascadeRestriction || $row->pr_cascade;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->mRestrictionsLoaded = true;
|
||||
MediaWikiServices::getInstance()->getRestrictionStore()->loadRestrictionsFromRows(
|
||||
$this, $rows, $oldFashionedRestrictions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load restrictions from the page_restrictions table
|
||||
*
|
||||
* @deprecated since 1.37, no public replacement
|
||||
*
|
||||
* @param string|null $oldFashionedRestrictions Comma-separated set of permission keys
|
||||
* indicating who can move or edit the page from the page table, (pre 1.10) rows.
|
||||
* Edit and move sections are separated by a colon
|
||||
|
|
@ -2958,79 +2702,18 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
* from the primary DB.
|
||||
*/
|
||||
public function loadRestrictions( $oldFashionedRestrictions = null, $flags = 0 ) {
|
||||
$readLatest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
|
||||
if ( $this->mRestrictionsLoaded && !$readLatest ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $this->getArticleID( $flags );
|
||||
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 = wfGetDB( DB_PRIMARY );
|
||||
$rows = $loadRestrictionsFromDb( $dbr );
|
||||
} else {
|
||||
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
|
||||
$rows = $cache->getWithSetCallback(
|
||||
// Page protections always leave a new null revision
|
||||
$cache->makeKey( 'page-restrictions', 'v1', $id, $this->getLatestRevID() ),
|
||||
$cache::TTL_DAY,
|
||||
static function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) {
|
||||
$dbr = wfGetDB( DB_REPLICA );
|
||||
|
||||
$setOpts += Database::getCacheSetOptions( $dbr );
|
||||
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
|
||||
if ( $lb->hasOrMadeRecentMasterChanges() ) {
|
||||
// @TODO: cleanup Title cache and caller assumption mess in general
|
||||
$ttl = WANObjectCache::TTL_UNCACHEABLE;
|
||||
}
|
||||
|
||||
return $loadRestrictionsFromDb( $dbr );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
|
||||
} else {
|
||||
$title_protection = $this->getTitleProtectionInternal();
|
||||
|
||||
if ( $title_protection ) {
|
||||
$now = wfTimestampNow();
|
||||
$expiry = wfGetDB( DB_REPLICA )->decodeExpiry( $title_protection['expiry'] );
|
||||
|
||||
if ( !$expiry || $expiry > $now ) {
|
||||
// Apply the restrictions
|
||||
$this->mRestrictionsExpiry['create'] = $expiry;
|
||||
$this->mRestrictions['create'] =
|
||||
explode( ',', trim( $title_protection['permission'] ) );
|
||||
} else { // Get rid of the old restrictions
|
||||
$this->mTitleProtection = false;
|
||||
}
|
||||
} else {
|
||||
$this->mRestrictionsExpiry['create'] = 'infinity';
|
||||
}
|
||||
$this->mRestrictionsLoaded = true;
|
||||
}
|
||||
MediaWikiServices::getInstance()->getRestrictionStore()->loadRestrictions( $this, $flags,
|
||||
$oldFashionedRestrictions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the protection cache in this object and force reload from the database.
|
||||
* This is used when updating protection from WikiPage::doUpdateRestrictions().
|
||||
*
|
||||
* @deprecated since 1.37, now internal
|
||||
*/
|
||||
public function flushRestrictions() {
|
||||
$this->mRestrictionsLoaded = false;
|
||||
$this->mTitleProtection = null;
|
||||
MediaWikiServices::getInstance()->getRestrictionStore()->flushRestrictions( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3344,9 +3027,6 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
} else {
|
||||
$this->mArticleID = (int)$id;
|
||||
}
|
||||
$this->mRestrictionsLoaded = false;
|
||||
$this->mRestrictions = [];
|
||||
$this->mOldRestrictions = false;
|
||||
$this->mRedirect = null;
|
||||
$this->mLength = -1;
|
||||
$this->mLatestID = false;
|
||||
|
|
@ -3358,6 +3038,7 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
|
|||
$this->mIsBigDeletion = null;
|
||||
|
||||
MediaWikiServices::getInstance()->getLinkCache()->clearLink( $this );
|
||||
MediaWikiServices::getInstance()->getRestrictionStore()->flushRestrictions( $this );
|
||||
}
|
||||
|
||||
public static function clearCaches() {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use MediaWiki\Block\Restriction\ActionRestriction;
|
|||
use MediaWiki\Block\Restriction\NamespaceRestriction;
|
||||
use MediaWiki\Block\Restriction\PageRestriction;
|
||||
use MediaWiki\Block\SystemBlock;
|
||||
use MediaWiki\Cache\CacheKeyHelper;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Revision\MutableRevisionRecord;
|
||||
use MediaWiki\Session\SessionId;
|
||||
|
|
@ -240,13 +241,18 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
|
|||
$this->setTitle( NS_MAIN, "test page" );
|
||||
$this->overrideUserPermissions( $this->user, [ "edit", "bogus", 'createpage' ] );
|
||||
|
||||
$this->title->mCascadeSources = [
|
||||
Title::makeTitle( NS_MAIN, "Bogus" ),
|
||||
Title::makeTitle( NS_MAIN, "UnBogus" )
|
||||
];
|
||||
$this->title->mCascadingRestrictions = [
|
||||
"bogus" => [ 'bogus', "sysop", "protect", "" ]
|
||||
];
|
||||
$rs = MediaWikiServices::getInstance()->getRestrictionStore();
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $rs );
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $this->title ) => [
|
||||
'cascade_sources' => [
|
||||
[
|
||||
Title::makeTitle( NS_MAIN, "Bogus" ),
|
||||
Title::makeTitle( NS_MAIN, "UnBogus" )
|
||||
], [
|
||||
"bogus" => [ 'bogus', "sysop", "protect", "" ],
|
||||
]
|
||||
],
|
||||
] ];
|
||||
|
||||
$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
|
||||
|
||||
|
|
@ -275,24 +281,29 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
|
|||
$expectedPermErrors,
|
||||
$expectedUserCan
|
||||
) {
|
||||
$this->setTitle( $namespace, "test page" );
|
||||
$this->title->mTitleProtection['permission'] = '';
|
||||
$this->title->mTitleProtection['user'] = $this->user->getId();
|
||||
$this->title->mTitleProtection['expiry'] = 'infinity';
|
||||
$this->title->mTitleProtection['reason'] = 'test';
|
||||
$this->title->mCascadeRestriction = false;
|
||||
$this->title->mRestrictionsLoaded = true;
|
||||
$this->setTitle( $namespace, "Test page" );
|
||||
|
||||
if ( isset( $titleOverrides['protectedPermission' ] ) ) {
|
||||
$this->title->mTitleProtection['permission'] = $titleOverrides['protectedPermission'];
|
||||
}
|
||||
if ( isset( $titleOverrides['interwiki'] ) ) {
|
||||
$this->title->mInterwiki = $titleOverrides['interwiki'];
|
||||
}
|
||||
$this->overrideUserPermissions( $this->user, $userPerms );
|
||||
|
||||
$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
|
||||
|
||||
$this->overrideUserPermissions( $this->user, $userPerms );
|
||||
$rs = MediaWikiServices::getInstance()->getRestrictionStore();
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $rs );
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $this->title ) => [
|
||||
'create_protection' => [
|
||||
'permission' => $titleOverrides['protectedPermission'] ?? '',
|
||||
'user' => $this->user->getId(),
|
||||
'expiry' => 'infinity',
|
||||
'reason' => 'test',
|
||||
],
|
||||
'has_cascading' => false,
|
||||
// XXX This is bogus, restrictions won't be empty if there's create protection
|
||||
'restrictions' => [],
|
||||
] ];
|
||||
|
||||
if ( isset( $titleOverrides['interwiki'] ) ) {
|
||||
$this->title->mInterwiki = $titleOverrides['interwiki'];
|
||||
}
|
||||
|
||||
$this->assertEquals(
|
||||
$expectedPermErrors,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Cache\CacheKeyHelper;
|
||||
use MediaWiki\Linker\LinkTarget;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Page\PageIdentity;
|
||||
use MediaWiki\Page\PageIdentityValue;
|
||||
use MediaWiki\Page\PageReference;
|
||||
use MediaWiki\Page\PageReferenceValue;
|
||||
use MediaWiki\Permissions\RestrictionStore;
|
||||
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
||||
use MediaWiki\User\UserIdentityValue;
|
||||
use Wikimedia\TestingAccessWrapper;
|
||||
|
||||
/**
|
||||
* @group Database
|
||||
|
|
@ -1414,10 +1417,9 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
$title = $this->getExistingTestPage( 'UTest1' )->getTitle();
|
||||
$title->loadRestrictions();
|
||||
$this->assertTrue( $title->areRestrictionsLoaded() );
|
||||
$this->assertSame(
|
||||
'infinity',
|
||||
$title->getRestrictionExpiry( 'create' )
|
||||
);
|
||||
$this->assertFalse( $title->getRestrictionExpiry( 'create' ),
|
||||
"Existing page can't have create protection" );
|
||||
$this->assertSame( 'infinity', $title->getRestrictionExpiry( 'edit' ) );
|
||||
$page = $this->getNonexistingTestPage( 'UTest1' );
|
||||
$title = $page->getTitle();
|
||||
$protectExpiry = wfTimestamp( TS_MW, time() + 10000 );
|
||||
|
|
@ -1429,7 +1431,7 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
'test',
|
||||
$this->getTestSysop()->getUser()
|
||||
);
|
||||
$title->mRestrictionsLoaded = false;
|
||||
$title->flushRestrictions();
|
||||
$title->loadRestrictions();
|
||||
$this->assertSame(
|
||||
$title->getRestrictionExpiry( 'create' ),
|
||||
|
|
@ -1484,17 +1486,133 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideRestrictionStoreForwarding
|
||||
* @covers Title::getFilteredRestrictionTypes
|
||||
* @covers Title::getRestrictionTypes
|
||||
* @covers Title::getTitleProtection
|
||||
* @covers Title::deleteTitleProtection
|
||||
* @covers Title::isSemiProtected
|
||||
* @covers Title::isProtected
|
||||
* @covers Title::isCascadeProtected
|
||||
* @covers Title::areCascadeProtectionSourcesLoaded
|
||||
* @covers Title::getCascadeProtectionSources
|
||||
* @covers Title::areRestrictionsLoaded
|
||||
* @covers Title::getRestrictions
|
||||
* @covers Title::getAllRestrictions
|
||||
* @covers Title::getRestrictionExpiry
|
||||
* @covers Title::areRestrictionsCascading
|
||||
* @covers Title::loadRestrictionsFromRows
|
||||
* @covers Title::loadRestrictions
|
||||
* @covers Title::flushRestrictions
|
||||
*/
|
||||
public function testRestrictionStoreForwarding(
|
||||
string $method, array $params, $return, array $options = []
|
||||
) {
|
||||
$expectedParams = $options['expectedParams'] ?? $params;
|
||||
|
||||
if ( isset( $options['static'] ) ) {
|
||||
$callee = 'Title';
|
||||
} else {
|
||||
$callee = $this->getExistingTestPage()->getTitle();
|
||||
$expectedParams = array_merge( [ $callee ], $expectedParams );
|
||||
}
|
||||
|
||||
$mockRestrictionStore = $this->createMock( RestrictionStore::class );
|
||||
|
||||
$expectedMethod = $options['expectedMethod'] ?? $method;
|
||||
|
||||
// Don't try to forward to a method that doesn't exist!
|
||||
$this->assertIsCallable( [ $mockRestrictionStore, $expectedMethod ] );
|
||||
|
||||
$expectedCall = $mockRestrictionStore->expects( $this->once() )
|
||||
->method( $expectedMethod )
|
||||
->with( ...$expectedParams );
|
||||
if ( !isset( $options['void'] ) ) {
|
||||
$expectedCall->willReturn( $return );
|
||||
}
|
||||
|
||||
$mockRestrictionStore->expects( $this->never() )
|
||||
->method( $this->anythingBut( $expectedMethod ) );
|
||||
|
||||
$this->setService( 'RestrictionStore', $mockRestrictionStore );
|
||||
|
||||
$options['expectedReturn'] = $options['expectedReturn'] ?? $return;
|
||||
|
||||
$comparisonMethod = isset( $options['weakCompareReturn'] ) ? 'assertEquals' : 'assertSame';
|
||||
|
||||
$this->$comparisonMethod( $options['expectedReturn'], [ $callee, $method ]( ...$params ) );
|
||||
}
|
||||
|
||||
public static function provideRestrictionStoreForwarding() {
|
||||
$pageIdentity = PageIdentityValue::localIdentity( 144, NS_MAIN, 'Sample' );
|
||||
$title = Title::castFromPageIdentity( $pageIdentity );
|
||||
return [
|
||||
[ 'getFilteredRestrictionTypes', [ true ], [ 'abc' ],
|
||||
[ 'static' => true, 'expectedMethod' => 'listAllRestrictionTypes' ] ],
|
||||
[ 'getFilteredRestrictionTypes', [ false ], [ 'def' ],
|
||||
[ 'static' => true, 'expectedMethod' => 'listAllRestrictionTypes' ] ],
|
||||
[ 'getRestrictionTypes', [], [ 'ghi' ],
|
||||
[ 'expectedMethod' => 'listApplicableRestrictionTypes' ] ],
|
||||
[ 'getTitleProtection', [], [ 'jkl' ], [ 'expectedMethod' => 'getCreateProtection' ] ],
|
||||
[ 'getTitleProtection', [], null,
|
||||
[ 'expectedMethod' => 'getCreateProtection', 'expectedReturn' => false ] ],
|
||||
[ 'deleteTitleProtection', [], null,
|
||||
[ 'expectedMethod' => 'deleteCreateProtection', 'void' => true ] ],
|
||||
[ 'isSemiProtected', [ 'phlebotomize' ], true ],
|
||||
[ 'isSemiProtected', [ 'splecotomize' ], false ],
|
||||
[ 'isProtected', [ 'strezotomize' ], true ],
|
||||
[ 'isProtected', [ 'chrelotomize' ], false ],
|
||||
[ 'isCascadeProtected', [], true ],
|
||||
[ 'isCascadeProtected', [], false ],
|
||||
[ 'areCascadeProtectionSourcesLoaded', [ true ], true, [ 'expectedParams' => [] ] ],
|
||||
[ 'areCascadeProtectionSourcesLoaded', [ true ], false, [ 'expectedParams' => [] ] ],
|
||||
[ 'areCascadeProtectionSourcesLoaded', [ false ], true, [ 'expectedParams' => [] ] ],
|
||||
[ 'areCascadeProtectionSourcesLoaded', [ false ], false, [ 'expectedParams' => [] ] ],
|
||||
[ 'getCascadeProtectionSources', [], [ [ $pageIdentity ], [ 'mno' ] ],
|
||||
[ 'expectedReturn' => [ [ $title ], [ 'mno' ] ], 'weakCompareReturn' => true ] ],
|
||||
[ 'getCascadeProtectionSources', [], [ [], [] ],
|
||||
[ 'expectedReturn' => [ false, [] ] ] ],
|
||||
[ 'getCascadeProtectionSources', [ true ], [ [ $pageIdentity ], [ 'mno' ] ],
|
||||
[ 'expectedParams' => [], 'expectedReturn' => [ [ $title ], [ 'mno' ] ],
|
||||
'weakCompareReturn' => true ] ],
|
||||
[ 'getCascadeProtectionSources', [ true ], [ [], [] ],
|
||||
[ 'expectedParams' => [], 'expectedReturn' => [ false, [] ] ] ],
|
||||
[ 'getCascadeProtectionSources', [ false ], false,
|
||||
[ 'expectedMethod' => 'isCascadeProtected', 'expectedParams' => [],
|
||||
'expectedReturn' => [ false, [] ] ] ],
|
||||
[ 'getCascadeProtectionSources', [ false ], true,
|
||||
[ 'expectedMethod' => 'isCascadeProtected', 'expectedParams' => [],
|
||||
'expectedReturn' => [ true, [] ] ] ],
|
||||
[ 'areRestrictionsLoaded', [], true ],
|
||||
[ 'areRestrictionsLoaded', [], false ],
|
||||
[ 'getRestrictions', [ 'stu' ], [ 'vwx' ] ],
|
||||
[ 'getAllRestrictions', [], [ 'yza' ] ],
|
||||
[ 'getRestrictionExpiry', [ 'bcd' ], 'efg' ],
|
||||
[ 'getRestrictionExpiry', [ 'hij' ], null, [ 'expectedReturn' => false ] ],
|
||||
[ 'areRestrictionsCascading', [], true ],
|
||||
[ 'areRestrictionsCascading', [], false ],
|
||||
[ 'loadRestrictionsFromRows', [ [ 'hij' ], 'klm' ], null, [ 'void' => true ] ],
|
||||
[ 'loadRestrictions', [ 'nop', 123 ], null,
|
||||
[ 'void' => true, 'expectedParams' => [ 123, 'nop' ] ] ],
|
||||
[ 'flushRestrictions', [], null, [ 'void' => true ] ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers Title::getRestrictions
|
||||
*/
|
||||
public function testGetRestrictions() {
|
||||
$title = $this->getExistingTestPage( 'UTest1' )->getTitle();
|
||||
$title->mRestrictions = [
|
||||
'a' => [ 'sysop' ],
|
||||
'b' => [ 'sysop' ],
|
||||
'c' => [ 'sysop' ]
|
||||
];
|
||||
$title->mRestrictionsLoaded = true;
|
||||
$rs = MediaWikiServices::getInstance()->getRestrictionStore();
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $rs );
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $title ) => [
|
||||
'restrictions' => [
|
||||
'a' => [ 'sysop' ],
|
||||
'b' => [ 'sysop' ],
|
||||
'c' => [ 'sysop' ]
|
||||
],
|
||||
] ];
|
||||
$this->assertArrayEquals( [ 'sysop' ], $title->getRestrictions( 'a' ) );
|
||||
$this->assertArrayEquals( [], $title->getRestrictions( 'error' ) );
|
||||
// TODO: maybe test if loadRestrictionsFromRows() is called?
|
||||
|
|
@ -1504,15 +1622,19 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
* @covers Title::getAllRestrictions
|
||||
*/
|
||||
public function testGetAllRestrictions() {
|
||||
$title = $this->getExistingTestPage( 'UTest1' )->getTitle();
|
||||
$title->mRestrictions = [
|
||||
$restrictions = [
|
||||
'a' => [ 'sysop' ],
|
||||
'b' => [ 'sysop' ],
|
||||
'c' => [ 'sysop' ]
|
||||
'c' => [ 'sysop' ],
|
||||
];
|
||||
$title->mRestrictionsLoaded = true;
|
||||
$title = $this->getExistingTestPage( 'UTest1' )->getTitle();
|
||||
$rs = MediaWikiServices::getInstance()->getRestrictionStore();
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $rs );
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $title ) => [
|
||||
'restrictions' => $restrictions
|
||||
] ];
|
||||
$this->assertArrayEquals(
|
||||
$title->mRestrictions,
|
||||
$restrictions,
|
||||
$title->getAllRestrictions()
|
||||
);
|
||||
}
|
||||
|
|
@ -1522,13 +1644,15 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
*/
|
||||
public function testGetRestrictionExpiry() {
|
||||
$title = $this->getExistingTestPage( 'UTest1' )->getTitle();
|
||||
$reflection = new ReflectionClass( $title );
|
||||
$reflection_property = $reflection->getProperty( 'mRestrictionsExpiry' );
|
||||
$reflection_property->setAccessible( true );
|
||||
$reflection_property->setValue( $title, [
|
||||
'a' => 'infinity', 'b' => 'infinity', 'c' => 'infinity'
|
||||
] );
|
||||
$title->mRestrictionsLoaded = true;
|
||||
$rs = MediaWikiServices::getInstance()->getRestrictionStore();
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $rs );
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $title ) => [
|
||||
'expiry' => [
|
||||
'a' => 'infinity', 'b' => 'infinity', 'c' => 'infinity'
|
||||
],
|
||||
// XXX This is bogus, restrictions will never be empty when expiry is not
|
||||
'restrictions' => [],
|
||||
] ];
|
||||
$this->assertSame( 'infinity', $title->getRestrictionExpiry( 'a' ) );
|
||||
$this->assertArrayEquals( [], $title->getRestrictions( 'error' ) );
|
||||
}
|
||||
|
|
@ -1538,7 +1662,6 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
*/
|
||||
public function testGetTitleProtection() {
|
||||
$title = $this->getNonexistingTestPage( 'UTest1' )->getTitle();
|
||||
$title->mTitleProtection = false;
|
||||
$this->assertFalse( $title->getTitleProtection() );
|
||||
}
|
||||
|
||||
|
|
@ -1547,17 +1670,19 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
*/
|
||||
public function testIsSemiProtected() {
|
||||
$title = $this->getExistingTestPage( 'UTest1' )->getTitle();
|
||||
$title->mRestrictions = [
|
||||
'edit' => [ 'sysop' ]
|
||||
];
|
||||
$this->setMwGlobals( [
|
||||
'wgSemiprotectedRestrictionLevels' => [ 'autoconfirmed' ],
|
||||
'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ]
|
||||
] );
|
||||
$rs = MediaWikiServices::getInstance()->getRestrictionStore();
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $rs );
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $title ) => [
|
||||
'restrictions' => [ 'edit' => [ 'sysop' ] ],
|
||||
] ];
|
||||
$this->assertFalse( $title->isSemiProtected( 'edit' ) );
|
||||
$title->mRestrictions = [
|
||||
'edit' => [ 'autoconfirmed' ]
|
||||
];
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $title ) => [
|
||||
'restrictions' => [ 'edit' => [ 'autoconfirmed' ] ],
|
||||
] ];
|
||||
$this->assertTrue( $title->isSemiProtected( 'edit' ) );
|
||||
}
|
||||
|
||||
|
|
@ -1578,13 +1703,15 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
|
||||
'wgRestrictionTypes' => [ 'create', 'edit', 'move', 'upload' ]
|
||||
] );
|
||||
$title->mRestrictions = [
|
||||
'edit' => [ 'sysop' ]
|
||||
];
|
||||
$this->assertFalse( $title->isProtected( 'edit' ) );
|
||||
$title->mRestrictions = [
|
||||
'edit' => [ 'test' ]
|
||||
];
|
||||
$rs = MediaWikiServices::getInstance()->getRestrictionStore();
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $rs );
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $title ) => [
|
||||
'restrictions' => [ 'edit' => [ 'sysop' ] ],
|
||||
] ];
|
||||
$this->assertTrue( $title->isProtected( 'edit' ) );
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $title ) => [
|
||||
'restrictions' => [ 'edit' => [ 'test' ] ],
|
||||
] ];
|
||||
$this->assertFalse( $title->isProtected( 'edit' ) );
|
||||
}
|
||||
|
||||
|
|
@ -1616,14 +1743,15 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
public function testIsCascadeProtected() {
|
||||
$page = $this->getExistingTestPage( 'UTest1' );
|
||||
$title = $page->getTitle();
|
||||
$reflection = new ReflectionClass( $title );
|
||||
$reflection_property = $reflection->getProperty( 'mHasCascadingRestrictions' );
|
||||
$reflection_property->setAccessible( true );
|
||||
$reflection_property->setValue( $title, true );
|
||||
$rs = MediaWikiServices::getInstance()->getRestrictionStore();
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $rs );
|
||||
$wrapper->cache = [ CacheKeyHelper::getKeyForPage( $title ) => [
|
||||
'has_cascading' => true,
|
||||
] ];
|
||||
$this->assertTrue( $title->isCascadeProtected() );
|
||||
$reflection_property->setValue( $title, null );
|
||||
$wrapper->cache = [];
|
||||
$this->assertFalse( $title->isCascadeProtected() );
|
||||
$reflection_property->setValue( $title, null );
|
||||
$wrapper->cache = [];
|
||||
$cascade = 1;
|
||||
$anotherPage = $this->getExistingTestPage( 'UTest2' );
|
||||
$anotherPage->doUserEditContent(
|
||||
|
|
@ -1643,6 +1771,7 @@ class TitleTest extends MediaWikiIntegrationTestCase {
|
|||
|
||||
/**
|
||||
* @covers Title::getCascadeProtectionSources
|
||||
* @group Broken
|
||||
*/
|
||||
public function testGetCascadeProtectionSources() {
|
||||
$page = $this->getExistingTestPage( 'UTest1' );
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Tests\Integration\Permissions;
|
||||
|
||||
use IDBAccessObject;
|
||||
use MediaWiki\Cache\CacheKeyHelper;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\Page\PageIdentityValue;
|
||||
use MediaWiki\Permissions\RestrictionStore;
|
||||
use MediaWikiIntegrationTestCase;
|
||||
use Title;
|
||||
use Wikimedia\TestingAccessWrapper;
|
||||
|
||||
/**
|
||||
* @group Database
|
||||
*
|
||||
* See \MediaWiki\Tests\Unit\Permissions\RestrictionStoreTest
|
||||
* for unit tests
|
||||
*
|
||||
* @coversDefaultClass \MediaWiki\Permissions\RestrictionStore
|
||||
*/
|
||||
class RestrictionStoreTest extends MediaWikiIntegrationTestCase {
|
||||
private const DEFAULT_RESTRICTION_TYPES = [ 'create', 'edit', 'move', 'upload' ];
|
||||
|
||||
/** @var WANObjectCache */
|
||||
private $wanCache;
|
||||
|
||||
/** @var ILoadBalancer */
|
||||
private $loadBalancer;
|
||||
|
||||
/** @var LinkCache */
|
||||
private $linkCache;
|
||||
|
||||
/** @var HookContainer */
|
||||
private $hookContainer;
|
||||
|
||||
/** @var CommentStore */
|
||||
private $commentStore;
|
||||
|
||||
/** @var PageStore */
|
||||
private $pageStore;
|
||||
|
||||
private static $testPageRestrictionSource;
|
||||
private static $testPageRestrictionCascade;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$services = $this->getServiceContainer();
|
||||
$this->wanCache = $services->getMainWANObjectCache();
|
||||
$this->loadBalancer = $services->getDBLoadBalancer();
|
||||
$this->linkCache = $services->getLinkCache();
|
||||
$this->commentStore = $services->getCommentStore();
|
||||
$this->hookContainer = $services->getHookContainer();
|
||||
$this->pageStore = $services->getPageStore();
|
||||
}
|
||||
|
||||
public function addDBDataOnce() {
|
||||
self::$testPageRestrictionCascade =
|
||||
$this->insertPage( 'Template:RestrictionStoreTestA', 'wooooooo' );
|
||||
$this->insertPage( 'Template:RestrictionStoreTestB', '{{RestrictionStoreTestA}}' );
|
||||
|
||||
self::$testPageRestrictionSource =
|
||||
$this->insertPage( 'RestrictionStoreTest_1', '{{RestrictionStoreTestB}}' );
|
||||
|
||||
$this->updateRestrictions( self::$testPageRestrictionSource['title'], [ 'edit' => 'sysop' ] );
|
||||
}
|
||||
|
||||
private function newRestrictionStore( array $options = [] ) {
|
||||
return new RestrictionStore(
|
||||
new ServiceOptions( RestrictionStore::CONSTRUCTOR_OPTIONS, $options + [
|
||||
'NamespaceProtection' => [],
|
||||
'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
|
||||
'RestrictionTypes' => self::DEFAULT_RESTRICTION_TYPES,
|
||||
'SemiprotectedRestrictionLevels' => [ 'autoconfirmed' ],
|
||||
] ),
|
||||
$this->wanCache,
|
||||
$this->loadBalancer,
|
||||
$this->linkCache,
|
||||
$this->commentStore,
|
||||
$this->hookContainer,
|
||||
$this->pageStore
|
||||
);
|
||||
}
|
||||
|
||||
private function updateRestrictions( $page, array $limit, int $cascade = 1 ) {
|
||||
$this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $page )
|
||||
->doUpdateRestrictions(
|
||||
$limit,
|
||||
[],
|
||||
$cascade,
|
||||
'test',
|
||||
$this->getTestSysop()->getUser()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getCascadeProtectionSources
|
||||
* @covers ::getCascadeProtectionSourcesInternal
|
||||
*/
|
||||
public function testGetCascadeProtectionSources() {
|
||||
$page = self::$testPageRestrictionCascade['title'];
|
||||
$pageSource = self::$testPageRestrictionSource['title'];
|
||||
|
||||
[ $sources, $restrictions ] = $this->newRestrictionStore()
|
||||
->getCascadeProtectionSources( $page );
|
||||
$this->assertCount( 1, $sources );
|
||||
$this->assertTrue( $pageSource->isSamePageAs( $sources[$pageSource->getId()] ) );
|
||||
$this->assertArrayEquals( [ 'edit' => [ 'sysop' ] ], $restrictions );
|
||||
|
||||
[ $sources, $restrictions ] = $this->newRestrictionStore()
|
||||
->getCascadeProtectionSources( $pageSource );
|
||||
$this->assertCount( 0, $sources );
|
||||
$this->assertCount( 0, $restrictions );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::loadRestrictions
|
||||
* @dataProvider provideLoadRestrictions
|
||||
*/
|
||||
public function testLoadRestrictions( $page, $expectedCacheSubmap, ?array $restrictions = null ) {
|
||||
$cacheKey = CacheKeyHelper::getKeyForPage( $page );
|
||||
|
||||
if ( $restrictions ) {
|
||||
$this->updateRestrictions( $page, $restrictions );
|
||||
}
|
||||
|
||||
$restrictionStore = $this->newRestrictionStore();
|
||||
$restrictionStore->loadRestrictions( $page );
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $restrictionStore );
|
||||
$this->assertArraySubmapSame(
|
||||
$expectedCacheSubmap,
|
||||
$wrapper->cache[$cacheKey]
|
||||
);
|
||||
}
|
||||
|
||||
public function provideLoadRestrictions(): array {
|
||||
return [
|
||||
'Regular page with restrictions' => [
|
||||
Title::newFromText( 'RestrictionStoreTest_1' ),
|
||||
[ 'restrictions' => [ 'edit' => [ 'sysop' ] ] ]
|
||||
],
|
||||
'Nonexistent page' => [
|
||||
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
|
||||
[ 'create_protection' => null ]
|
||||
],
|
||||
'Nonexistent page with restrictions' => [
|
||||
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
|
||||
[ 'create_protection' => [ 'expiry' => 'infinity' ] ],
|
||||
[ 'create' => 'sysop' ]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::loadRestrictions
|
||||
*/
|
||||
public function testLoadRestrictions_latest() {
|
||||
$pageSource = self::$testPageRestrictionSource['title'];
|
||||
$cacheKey = CacheKeyHelper::getKeyForPage( $pageSource );
|
||||
|
||||
$restrictionStore = $this->newRestrictionStore();
|
||||
$restrictionStore->loadRestrictions( $pageSource );
|
||||
$wrapper = TestingAccessWrapper::newFromObject( $restrictionStore );
|
||||
$this->assertArraySubmapSame(
|
||||
[ 'restrictions' => [ 'edit' => [ 'sysop' ] ] ],
|
||||
$wrapper->cache[$cacheKey]
|
||||
);
|
||||
|
||||
$this->updateRestrictions( $pageSource, [ 'move' => 'sysop' ] );
|
||||
$restrictionStore->loadRestrictions( $pageSource, IDBAccessObject::READ_LATEST );
|
||||
$this->assertArraySubmapSame(
|
||||
[ 'restrictions' => [ 'move' => [ 'sysop' ] ] ],
|
||||
$wrapper->cache[$cacheKey]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
namespace MediaWiki\Tests\Unit;
|
||||
|
||||
use CommentStore;
|
||||
use ConfiguredReadOnlyMode;
|
||||
use GenderCache;
|
||||
use Interwiki;
|
||||
|
|
@ -493,4 +494,19 @@ trait DummyServicesTrait {
|
|||
} );
|
||||
return $mock;
|
||||
}
|
||||
|
||||
private function getDummyCommentStore(): CommentStore {
|
||||
$mockLang = $this->createNoOpMock( Language::class,
|
||||
[ 'truncateForVisual', 'truncateForDatabase' ] );
|
||||
$mockLang->method( $this->logicalOr( 'truncateForDatabase', 'truncateForVisual' ) )
|
||||
->willReturnCallback(
|
||||
static function ( string $text, int $limit ): string {
|
||||
if ( strlen( $text ) > $limit - 3 ) {
|
||||
return substr( $text, 0, $limit - 3 ) . '...';
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
);
|
||||
return new CommentStore( $mockLang, MIGRATION_NEW );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
979
tests/phpunit/unit/includes/Permissions/RestrictionStoreTest.php
Normal file
979
tests/phpunit/unit/includes/Permissions/RestrictionStoreTest.php
Normal file
|
|
@ -0,0 +1,979 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Tests\Unit\Permissions;
|
||||
|
||||
use DatabaseTestHelper;
|
||||
use LinkCache;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\Page\PageIdentity;
|
||||
use MediaWiki\Page\PageIdentityValue;
|
||||
use MediaWiki\Page\PageReferenceValue;
|
||||
use MediaWiki\Page\PageStore;
|
||||
use MediaWiki\Permissions\RestrictionStore;
|
||||
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
||||
use MediaWikiUnitTestCase;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use RuntimeException;
|
||||
use Title;
|
||||
use UnexpectedValueException;
|
||||
use WANObjectCache;
|
||||
use Wikimedia\Assert\PreconditionException;
|
||||
use Wikimedia\Rdbms\IDatabase;
|
||||
use Wikimedia\Rdbms\ILoadBalancer;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \MediaWiki\Permissions\RestrictionStore
|
||||
*/
|
||||
class RestrictionStoreTest extends MediaWikiUnitTestCase {
|
||||
use DummyServicesTrait;
|
||||
|
||||
private const DEFAULT_RESTRICTION_TYPES = [ 'create', 'edit', 'move', 'upload' ];
|
||||
|
||||
/**
|
||||
* @param array $expectedCalls E.g.:
|
||||
* [
|
||||
* DB_REPLICA => [
|
||||
* 'update' => callback, # to be called exactly once
|
||||
* 'delete' => [ callback, 2 ], # to be called exactly twice
|
||||
* 'select' => [ callback, -1 ], # may be called 0 or more times
|
||||
* ],
|
||||
* ]
|
||||
* @return ILoadBalancer
|
||||
*/
|
||||
private function newMockLoadBalancer( array $expectedCalls = [] ): ILoadBalancer {
|
||||
if ( !isset( $expectedCalls[DB_REPLICA] ) ) {
|
||||
$expectedCalls[DB_REPLICA] = [];
|
||||
}
|
||||
$expectedCalls[DB_REPLICA]['decodeExpiry'] = [
|
||||
static function ( string $expiry ): string {
|
||||
return $expiry;
|
||||
},
|
||||
-1
|
||||
];
|
||||
|
||||
$dbs = [];
|
||||
foreach ( $expectedCalls as $index => $calls ) {
|
||||
$dbs[$index] = $this->createNoOpMock( IDatabase::class, array_keys( $calls ) );
|
||||
foreach ( $calls as $method => $callback ) {
|
||||
$count = 1;
|
||||
if ( is_array( $callback ) ) {
|
||||
[ $callback, $count ] = $callback;
|
||||
}
|
||||
$dbs[$index]->expects( $count < 0 ? $this->any() : $this->exactly( $count ) )
|
||||
->method( $method )->willReturnCallback( $callback );
|
||||
}
|
||||
}
|
||||
|
||||
$lb = $this->createMock( ILoadBalancer::class, [ 'getConnectionRef' ] );
|
||||
$lb->method( 'getConnectionRef' )->willReturnCallback(
|
||||
function ( int $index ) use ( $dbs ): IDatabase {
|
||||
$this->assertArrayHasKey( $index, $dbs );
|
||||
return $dbs[$index];
|
||||
}
|
||||
);
|
||||
|
||||
return $lb;
|
||||
}
|
||||
|
||||
private function newRestrictionStore( array $options = [] ): RestrictionStore {
|
||||
return new RestrictionStore(
|
||||
new ServiceOptions( RestrictionStore::CONSTRUCTOR_OPTIONS, $options + [
|
||||
'NamespaceProtection' => [],
|
||||
'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
|
||||
'RestrictionTypes' => self::DEFAULT_RESTRICTION_TYPES,
|
||||
'SemiprotectedRestrictionLevels' => [ 'autoconfirmed' ],
|
||||
] ),
|
||||
$this->createNoOpMock( WANObjectCache::class ),
|
||||
$this->newMockLoadBalancer( $options['db'] ?? [] ),
|
||||
// @todo test that these calls work correctly
|
||||
$this->createNoOpMock( LinkCache::class, [ 'addLinkObj', 'getGoodLinkFieldObj' ] ),
|
||||
$this->getDummyCommentStore(),
|
||||
$this->createHookContainer( isset( $options['hookFn'] )
|
||||
? [ 'TitleGetRestrictionTypes' => $options['hookFn'] ]
|
||||
: [] ),
|
||||
$this->createNoOpMock( PageStore::class )
|
||||
);
|
||||
}
|
||||
|
||||
private static function newImproperPageIdentity(
|
||||
int $ns, string $dbKey, $wikiId = PageIdentity::LOCAL
|
||||
): PageIdentity {
|
||||
// PageIdentityValue doesn't allow negative namespaces, and Title::exists accesses services
|
||||
// for hooks, so we need another solution to unit-test PageIdentity objects with negative
|
||||
// namespaces (until they cease to exist).
|
||||
return new class( $ns, $dbKey, $wikiId ) extends PageReferenceValue implements PageIdentity {
|
||||
public function getId( $wikiId = PageIdentity::LOCAL ): int {
|
||||
throw new RuntimeException;
|
||||
}
|
||||
|
||||
public function canExist(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function exists(): bool {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::<public>
|
||||
* @dataProvider provideNonLocalPage
|
||||
*/
|
||||
public function testNonLocalPage( string $method, ...$extraArgs ) {
|
||||
$this->expectException( PreconditionException::class );
|
||||
$this->expectExceptionMessage( 'otherwiki' );
|
||||
|
||||
$obj = $this->newRestrictionStore();
|
||||
$page = new PageIdentityValue( 1, NS_MAIN, 'X', 'otherwiki' );
|
||||
$obj->$method( $page, ...$extraArgs );
|
||||
}
|
||||
|
||||
public function provideNonLocalPage() {
|
||||
// We programmatically get all public methods whose first parameter is a PageIdentity. This
|
||||
// way we'll make sure to include any new methods that are added in the future.
|
||||
$ret = [];
|
||||
$methods = ( new ReflectionClass( RestrictionStore::class ) )
|
||||
->getMethods( ReflectionMethod::IS_PUBLIC );
|
||||
foreach ( $methods as $method ) {
|
||||
$params = $method->getParameters();
|
||||
if ( !$params[0]->hasType() || $params[0]->getType()->getName() !== PageIdentity::class ) {
|
||||
continue;
|
||||
}
|
||||
$ret[$method->getName()] = [ $method->getName() ];
|
||||
|
||||
foreach ( array_slice( $params, 1 ) as $param ) {
|
||||
// Extra required arguments
|
||||
if ( $param->isOptional() ) {
|
||||
break;
|
||||
}
|
||||
switch ( $param->getType()->getName() ) {
|
||||
case 'string':
|
||||
$ret[$method->getName()][] = 'x';
|
||||
break;
|
||||
case 'array':
|
||||
$ret[$method->getName()][] = [];
|
||||
break;
|
||||
default:
|
||||
throw new UnexpectedValueException(
|
||||
"{$param->getType()->getName} type not supported" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getRestrictions
|
||||
* @dataProvider provideGetRestrictions
|
||||
*/
|
||||
public function testGetRestrictions(
|
||||
array $expected,
|
||||
PageIdentity $page,
|
||||
string $action,
|
||||
?array $rowsToLoad,
|
||||
array $options = []
|
||||
): void {
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
if ( is_array( $rowsToLoad ) ) {
|
||||
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
|
||||
}
|
||||
$this->assertSame( $expected, $obj->getRestrictions( $page, $action ) );
|
||||
}
|
||||
|
||||
public function provideGetRestrictions(): array {
|
||||
$all = $this->provideGetAllRestrictions();
|
||||
$ret = [];
|
||||
|
||||
foreach ( $all as $name => $arr ) {
|
||||
[ $expected, $page ] = $arr;
|
||||
|
||||
$actions = array_merge( self::DEFAULT_RESTRICTION_TYPES, [ 'vaporize' ],
|
||||
array_keys( $expected ) );
|
||||
foreach ( $actions as $action ) {
|
||||
$ret["$name ($action)"] =
|
||||
array_merge(
|
||||
[ $expected[$action] ?? [], $page, $action ],
|
||||
array_slice( $arr, 2 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::getAllRestrictions
|
||||
* @covers ::loadRestrictions
|
||||
* @covers ::loadRestrictionsFromRows
|
||||
* @dataProvider provideGetAllRestrictions
|
||||
*/
|
||||
public function testGetAllRestrictions(
|
||||
array $expected, PageIdentity $page, ?array $rowsToLoad, array $options = []
|
||||
): void {
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
if ( is_array( $rowsToLoad ) ) {
|
||||
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
|
||||
}
|
||||
$this->assertSame( $expected, $obj->getAllRestrictions( $page ) );
|
||||
}
|
||||
|
||||
public function provideGetAllRestrictions(): array {
|
||||
return [
|
||||
'Special page' => [
|
||||
[],
|
||||
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
|
||||
null,
|
||||
],
|
||||
'Media page' => [
|
||||
[],
|
||||
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
|
||||
null,
|
||||
],
|
||||
'Simple existing unprotected page' => [
|
||||
[ 'edit' => [], 'move' => [] ],
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[],
|
||||
],
|
||||
'Simple existing protected page' => [
|
||||
[ 'edit' => [ 'sysop', 'bureaucrat' ], 'move' => [ 'sysop' ] ],
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop,bureaucrat',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
],
|
||||
],
|
||||
'Protection type not allowed' => [
|
||||
[ 'edit' => [ 'sysop' ] ],
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
],
|
||||
[ 'RestrictionTypes' => [ 'create', 'edit', 'upload' ] ],
|
||||
],
|
||||
'Expired protection' => [
|
||||
[ 'edit' => [], 'move' => [ 'sysop' ] ],
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => '20200101000000', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getRestrictionExpiry
|
||||
* @dataProvider provideGetRestrictionExpiry
|
||||
*/
|
||||
public function testGetRestrictionExpiry(
|
||||
?string $expected,
|
||||
PageIdentity $page,
|
||||
string $action,
|
||||
?array $rowsToLoad,
|
||||
array $options = []
|
||||
): void {
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
if ( is_array( $rowsToLoad ) ) {
|
||||
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
|
||||
}
|
||||
$this->assertSame( $expected, $obj->getRestrictionExpiry( $page, $action ) );
|
||||
}
|
||||
|
||||
public function provideGetRestrictionExpiry(): array {
|
||||
return [
|
||||
'Special page' => [
|
||||
null,
|
||||
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
|
||||
'edit',
|
||||
null,
|
||||
],
|
||||
'Media page' => [
|
||||
null,
|
||||
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
|
||||
'edit',
|
||||
null,
|
||||
],
|
||||
'Simple existing unprotected page' => [
|
||||
'infinity',
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
'edit',
|
||||
[],
|
||||
],
|
||||
'Simple existing protected page (edit)' => [
|
||||
'20760101000000',
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
'edit',
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => '20760101000000', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
],
|
||||
],
|
||||
'Simple existing protected page (move)' => [
|
||||
'20670101000000',
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
'move',
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => '20670101000000', 'pr_cascade' => 0 ],
|
||||
],
|
||||
],
|
||||
'Simple existing protected page (unrecognized)' => [
|
||||
null,
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
'unrecognized',
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
],
|
||||
],
|
||||
'Simple existing expired protected page (edit)' => [
|
||||
'infinity',
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
'edit',
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => '20160101000000', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
],
|
||||
],
|
||||
'Simple existing expired protected page (move)' => [
|
||||
'infinity',
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
'move',
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => '20170101000000', 'pr_cascade' => 0 ],
|
||||
],
|
||||
],
|
||||
'Simple existing expired protected page (unrecognized)' => [
|
||||
null,
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
'unrecognized',
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => '20170101000000', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'unrecognized', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => '20170101000000', 'pr_cascade' => 0 ],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getCreateProtection
|
||||
* @covers ::getCreateProtectionInternal
|
||||
* @dataProvider provideGetCreateProtection
|
||||
*/
|
||||
public function testGetCreateProtection(
|
||||
?array $expected, PageIdentity $page, $return, array $options = []
|
||||
): void {
|
||||
if ( $page->canExist() && !$page->exists() ) {
|
||||
$options['db'] = [ DB_REPLICA => [ 'selectRow' =>
|
||||
function (
|
||||
$table, $vars, $conds, string $fname, $options = [], $join_conds = []
|
||||
) use ( $page, $return ) {
|
||||
$options = (array)$options;
|
||||
$options['LIMIT'] = 1;
|
||||
$db = new DatabaseTestHelper( __CLASS__ );
|
||||
$sql = trim( preg_replace( '/\s+/', ' ', $db->selectSQLText(
|
||||
$table, $vars, $conds, $fname, $options, $join_conds ) ) );
|
||||
$this->assertSame(
|
||||
'SELECT pt_user,pt_expiry,pt_create_perm,comment_pt_reason.comment_text ' .
|
||||
'AS pt_reason_text,comment_pt_reason.comment_data AS pt_reason_data,' .
|
||||
'comment_pt_reason.comment_id AS pt_reason_cid ' .
|
||||
'FROM protected_titles JOIN comment comment_pt_reason ON ' .
|
||||
'((comment_pt_reason.comment_id = pt_reason_id)) ' .
|
||||
"WHERE pt_namespace = {$page->getNamespace()} AND " .
|
||||
"pt_title = '{$page->getDBkey()}' LIMIT 1",
|
||||
$sql );
|
||||
|
||||
return is_array( $return ) ? (object)$return : $return;
|
||||
}
|
||||
] ];
|
||||
}
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
$this->assertSame( $expected, $obj->getCreateProtection( $page ) );
|
||||
}
|
||||
|
||||
public function provideGetCreateProtection(): array {
|
||||
$ret = [
|
||||
'Special page' => [ null, self::newImproperPageIdentity( NS_SPECIAL, 'X' ), null ],
|
||||
'Media page' => [ null, self::newImproperPageIdentity( NS_MEDIA, 'X' ), null ],
|
||||
'Existing page' => [ null, PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ), null ],
|
||||
'Unprotected' => [ null, PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ), false ],
|
||||
];
|
||||
$protectedTests = [
|
||||
'sysop' => 'editprotected',
|
||||
'autoconfirmed' => 'editsemiprotected',
|
||||
'editprotected' => 'editprotected',
|
||||
'editsemiprotected' => 'editsemiprotected',
|
||||
'custom' => 'custom',
|
||||
];
|
||||
foreach ( $protectedTests as $db => $returned ) {
|
||||
$ret["Protected ($db)"] = [
|
||||
[
|
||||
'user' => 123,
|
||||
'expiry' => 'infinity',
|
||||
'permission' => $returned,
|
||||
'reason' => 'reason',
|
||||
],
|
||||
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
|
||||
[
|
||||
'pt_user' => 123,
|
||||
'pt_expiry' => 'infinity',
|
||||
'pt_create_perm' => $db,
|
||||
'pt_reason_id' => 456,
|
||||
'pt_reason_data' => '{}',
|
||||
'pt_reason_text' => 'reason',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::deleteCreateProtection
|
||||
* @dataProvider provideDeleteCreateProtection
|
||||
*/
|
||||
public function testDeleteCreateProtection( PageIdentity $page ): void {
|
||||
$obj = $this->newRestrictionStore( [ 'db' => [ DB_PRIMARY => [ 'delete' =>
|
||||
function ( string $table, array $where, string $method ) use ( $page ): bool {
|
||||
$this->assertSame( 'protected_titles', $table );
|
||||
$this->assertSame(
|
||||
[ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ],
|
||||
$where
|
||||
);
|
||||
return true;
|
||||
}
|
||||
] ] ] );
|
||||
|
||||
$obj->deleteCreateProtection( $page );
|
||||
}
|
||||
|
||||
public function provideDeleteCreateProtection(): array {
|
||||
return [
|
||||
// Most of these don't actually make sense, but test current behavior regardless.
|
||||
'Special page' => [ self::newImproperPageIdentity( NS_SPECIAL, 'X' ) ],
|
||||
'Media page' => [ self::newImproperPageIdentity( NS_MEDIA, 'X' ) ],
|
||||
'Nonexistent page' => [ PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ) ],
|
||||
'Existing page' => [ PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ) ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isSemiProtected
|
||||
* @dataProvider provideIsSemiProtected
|
||||
*/
|
||||
public function testIsSemiProtected(
|
||||
bool $expected, ?array $rowsToLoad, array $options = []
|
||||
): void {
|
||||
$page = $options['page'] ?? PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' );
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
if ( is_array( $rowsToLoad ) ) {
|
||||
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
|
||||
}
|
||||
$this->assertSame( $expected, $obj->isSemiProtected( $page, 'edit' ) );
|
||||
}
|
||||
|
||||
public function provideIsSemiProtected(): array {
|
||||
return [
|
||||
'Special page' => [ false, null,
|
||||
[ 'page' => self::newImproperPageIdentity( NS_SPECIAL, 'X' ) ] ],
|
||||
'Media page' => [ false, null,
|
||||
[ 'page' => self::newImproperPageIdentity( NS_MEDIA, 'X' ) ] ],
|
||||
'Unprotected page' => [
|
||||
false,
|
||||
[ (object)[ 'pr_type' => 'move', 'pr_level' => 'autoconfirmed',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
],
|
||||
'Semiprotected page' => [
|
||||
true,
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
],
|
||||
],
|
||||
'Fully protected page' => [
|
||||
false,
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'autoconfirmed',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
],
|
||||
],
|
||||
'No semiprotection configured' => [
|
||||
false,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
[ 'SemiprotectedRestrictionLevels' => [] ],
|
||||
],
|
||||
'Config with editsemiprotected' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
[ 'SemiprotectedRestrictionLevels' => [ 'editsemiprotected' ] ],
|
||||
],
|
||||
'Data with editsemiprotected' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'editsemiprotected',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
],
|
||||
'Config and data with editsemiprotected' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'editsemiprotected',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
[ 'SemiprotectedRestrictionLevels' => [ 'editsemiprotected' ] ],
|
||||
],
|
||||
'Semiprotection plus other protection level' => [
|
||||
false,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed,superman',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
[ 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', 'superman' ] ],
|
||||
],
|
||||
'Two semiprotections' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed,superman',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
[ 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', 'superman' ],
|
||||
'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', 'superman' ] ],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isProtected
|
||||
* @dataProvider provideIsProtected
|
||||
*/
|
||||
public function testIsProtected(
|
||||
bool $expected, ?array $rowsToLoad, array $options = []
|
||||
): void {
|
||||
$page = $options['page'] ?? PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' );
|
||||
$action = $options['action'] ?? '';
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
if ( is_array( $rowsToLoad ) ) {
|
||||
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
|
||||
}
|
||||
$this->assertSame( $expected, $obj->isProtected( $page, $action ) );
|
||||
}
|
||||
|
||||
public function provideIsProtected(): array {
|
||||
return [
|
||||
'Special page' => [ true, null,
|
||||
[ 'page' => self::newImproperPageIdentity( NS_SPECIAL, 'X' ) ] ],
|
||||
'Media page' => [ false, null,
|
||||
[ 'page' => self::newImproperPageIdentity( NS_MEDIA, 'X' ) ] ],
|
||||
'Unprotected page' => [ false, [] ],
|
||||
'Semiprotected page' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'autoconfirmed',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
],
|
||||
'Fully protected page' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
],
|
||||
'Protected against empty string' => [
|
||||
false,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => '',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
],
|
||||
'Unrecognized protection' => [
|
||||
false,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'unrecognized',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
],
|
||||
'Unrecognized plus recognized protection' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop,unrecognized',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
],
|
||||
|
||||
'Check unrecognized protection type' => [
|
||||
false,
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
(object)[ 'pr_type' => 'unrecognized', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
],
|
||||
[ 'action' => 'unrecognized' ],
|
||||
],
|
||||
'Check custom protection type' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'custom', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ], ],
|
||||
[ 'action' => 'custom', 'RestrictionTypes' =>
|
||||
array_merge( self::DEFAULT_RESTRICTION_TYPES, [ 'custom' ] ) ],
|
||||
],
|
||||
'Check custom protection level' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'custom',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ], ],
|
||||
[ 'action' => 'edit', 'RestrictionLevels' =>
|
||||
[ '', 'autoconfirmed', 'sysop', 'custom' ] ],
|
||||
],
|
||||
|
||||
'Check edit protection of edit-protected page' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
[ 'action' => 'edit' ],
|
||||
],
|
||||
'Check move protection of edit-protected page' => [
|
||||
false,
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
[ 'action' => 'move' ],
|
||||
],
|
||||
'Check move protection of move-protected page' => [
|
||||
true,
|
||||
[ (object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
[ 'action' => 'move' ],
|
||||
],
|
||||
'Check edit protection of move-protected page' => [
|
||||
false,
|
||||
[ (object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ] ],
|
||||
[ 'action' => 'edit' ],
|
||||
],
|
||||
'Check edit protection of edit- and move-protected page' => [
|
||||
true,
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
],
|
||||
[ 'action' => 'edit' ],
|
||||
],
|
||||
'Check move protection of edit- and move-protected page' => [
|
||||
true,
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => '0' ],
|
||||
],
|
||||
[ 'action' => 'move' ],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::listApplicableRestrictionTypes
|
||||
* @dataProvider provideListApplicableRestrictionTypes
|
||||
*/
|
||||
public function testListApplicableRestrictionTypes(
|
||||
array $expected, PageIdentity $page, array $options = []
|
||||
): void {
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
|
||||
$this->assertSame( $expected, $obj->listApplicableRestrictionTypes( $page ) );
|
||||
}
|
||||
|
||||
public function provideListApplicableRestrictionTypes(): array {
|
||||
$expandedRestrictions = array_merge( self::DEFAULT_RESTRICTION_TYPES, [ 'liquify' ] );
|
||||
return [
|
||||
'Special page' => [
|
||||
[],
|
||||
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
|
||||
],
|
||||
'Media page' => [
|
||||
[],
|
||||
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
|
||||
],
|
||||
'Nonexistent page' => [
|
||||
[ 'create' ],
|
||||
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
|
||||
],
|
||||
'Existing page' => [
|
||||
[ 'edit', 'move' ],
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
],
|
||||
'Nonexistent file' => [
|
||||
[ 'create', 'upload' ],
|
||||
PageIdentityValue::localIdentity( 0, NS_FILE, 'X' ),
|
||||
],
|
||||
'Existing file' => [
|
||||
[ 'edit', 'move', 'upload' ],
|
||||
PageIdentityValue::localIdentity( 1, NS_FILE, 'X' ),
|
||||
],
|
||||
|
||||
'Nonexistent page with no create' => [
|
||||
[],
|
||||
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
|
||||
[ 'RestrictionTypes' => [ 'edit', 'move', 'upload' ] ],
|
||||
],
|
||||
'Existing page with no move' => [
|
||||
[ 'edit' ],
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[ 'RestrictionTypes' => [ 'create', 'edit', 'upload' ] ],
|
||||
],
|
||||
'Nonexistent file with no upload' => [
|
||||
[ 'create' ],
|
||||
PageIdentityValue::localIdentity( 0, NS_FILE, 'X' ),
|
||||
[ 'RestrictionTypes' => [ 'create', 'edit', 'move' ] ],
|
||||
],
|
||||
|
||||
'Special page with extra type' => [
|
||||
[],
|
||||
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
|
||||
[ 'RestrictionTypes' => $expandedRestrictions ],
|
||||
],
|
||||
'Media page with extra type' => [
|
||||
[],
|
||||
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
|
||||
[ 'RestrictionTypes' => $expandedRestrictions ],
|
||||
],
|
||||
'Nonexistent page with extra type' => [
|
||||
[ 'create' ],
|
||||
PageIdentityValue::localIdentity( 0, NS_MAIN, 'X' ),
|
||||
[ 'RestrictionTypes' => $expandedRestrictions ],
|
||||
],
|
||||
'Existing page with extra type' => [
|
||||
[ 'edit', 'move', 'liquify' ],
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[ 'RestrictionTypes' => $expandedRestrictions ],
|
||||
],
|
||||
'Nonexistent file with extra type' => [
|
||||
[ 'create', 'upload' ],
|
||||
PageIdentityValue::localIdentity( 0, NS_FILE, 'X' ),
|
||||
[ 'RestrictionTypes' => $expandedRestrictions ],
|
||||
],
|
||||
'Existing file with extra type' => [
|
||||
[ 'edit', 'move', 'upload', 'liquify' ],
|
||||
PageIdentityValue::localIdentity( 1, NS_FILE, 'X' ),
|
||||
[ 'RestrictionTypes' => $expandedRestrictions ],
|
||||
],
|
||||
|
||||
'Hook' => [
|
||||
[ 'move', 'liquify' ],
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[ 'hookFn' => function ( Title $title, array &$types ): bool {
|
||||
self::assertEquals( Title::castFromPageIdentity(
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ) ), $title );
|
||||
self::assertSame( [ 'edit', 'move' ], $types );
|
||||
|
||||
$types = [ 'move', 'liquify' ];
|
||||
|
||||
return false;
|
||||
} ],
|
||||
],
|
||||
'Hook not run for special page' => [
|
||||
[],
|
||||
self::newImproperPageIdentity( NS_SPECIAL, 'X' ),
|
||||
[ 'hookFn' => function () {
|
||||
$this->fail( 'Should be unreached' );
|
||||
} ],
|
||||
],
|
||||
'Hook not run for media page' => [
|
||||
[],
|
||||
self::newImproperPageIdentity( NS_MEDIA, 'X' ),
|
||||
[ 'hookFn' => function () {
|
||||
$this->fail( 'Should be unreached' );
|
||||
} ],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::listAllRestrictionTypes
|
||||
* @dataProvider provideListAllRestrictionTypes
|
||||
*/
|
||||
public function testListAllRestrictionTypes(
|
||||
array $expected, array $args, array $options = []
|
||||
) {
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
$this->assertSame( $expected, $obj->listAllRestrictionTypes( ...$args ) );
|
||||
}
|
||||
|
||||
public function provideListAllRestrictionTypes() {
|
||||
$expandedRestrictions = array_merge( self::DEFAULT_RESTRICTION_TYPES, [ 'solidify' ] );
|
||||
return [
|
||||
'Exists' => [ [ 'edit', 'move', 'upload' ], [ true ] ],
|
||||
'Default is exists' => [ [ 'edit', 'move', 'upload' ], [] ],
|
||||
'Nonexistent' => [ [ 'create', 'upload' ], [ false ] ],
|
||||
|
||||
'Exists with extra restriction type' => [
|
||||
[ 'edit', 'move', 'upload', 'solidify' ],
|
||||
[ true ],
|
||||
[ 'RestrictionTypes' => $expandedRestrictions ],
|
||||
],
|
||||
'Default is exists with extra restriction type' => [
|
||||
[ 'edit', 'move', 'upload', 'solidify' ],
|
||||
[],
|
||||
[ 'RestrictionTypes' => $expandedRestrictions ],
|
||||
],
|
||||
'Nonexistent with extra restriction type' => [
|
||||
[ 'create', 'upload' ],
|
||||
[ false ],
|
||||
[ 'RestrictionTypes' => $expandedRestrictions ],
|
||||
],
|
||||
|
||||
'Exists with no edit' => [
|
||||
[ 'move', 'upload' ],
|
||||
[ true ],
|
||||
[ 'RestrictionTypes' => [ 'create', 'move', 'upload' ] ],
|
||||
],
|
||||
'Exists with only create' => [
|
||||
[],
|
||||
[ true ],
|
||||
[ 'RestrictionTypes' => [ 'create' ] ],
|
||||
],
|
||||
'Nonexistent with no create' => [
|
||||
[ 'upload' ],
|
||||
[ false ],
|
||||
[ 'RestrictionTypes' => [ 'edit', 'move', 'upload', 'solidify' ] ],
|
||||
],
|
||||
'Nonexistent with no upload' => [
|
||||
[ 'create' ],
|
||||
[ false ],
|
||||
[ 'RestrictionTypes' => [ 'create', 'edit', 'move', 'solidify' ] ],
|
||||
],
|
||||
'Nonexistent with no create or upload' => [
|
||||
[],
|
||||
[ false ],
|
||||
[ 'RestrictionTypes' => [ 'edit', 'move', 'solidify' ] ],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::areRestrictionsLoaded
|
||||
* @covers ::loadRestrictionsFromRows
|
||||
* @dataProvider provideAreRestrictionsLoaded
|
||||
*/
|
||||
public function testAreRestrictionsLoaded(
|
||||
bool $expected, PageIdentity $page, ?array $rowsToLoad = null, array $options = []
|
||||
): void {
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
if ( is_array( $rowsToLoad ) ) {
|
||||
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
|
||||
}
|
||||
$this->assertSame( $expected, $obj->areRestrictionsLoaded( $page ) );
|
||||
}
|
||||
|
||||
public function provideAreRestrictionsLoaded(): array {
|
||||
return [
|
||||
'Special page' => [ false, self::newImproperPageIdentity( NS_SPECIAL, 'X' ) ],
|
||||
'Media page' => [ false, self::newImproperPageIdentity( NS_MEDIA, 'X' ) ],
|
||||
'Regular page' => [ false, PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ) ],
|
||||
'Regular page with no restrictions' =>
|
||||
[ true, PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ), [] ],
|
||||
'Regular page with restrictions' => [
|
||||
true,
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ] ],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::areRestrictionsCascading
|
||||
* @covers ::loadRestrictionsFromRows
|
||||
* @dataProvider provideAreRestrictionsCascading
|
||||
*/
|
||||
public function testAreRestrictionsCascading(
|
||||
bool $expected, PageIdentity $page, ?array $rowsToLoad, array $options = []
|
||||
): void {
|
||||
$obj = $this->newRestrictionStore( $options );
|
||||
if ( is_array( $rowsToLoad ) ) {
|
||||
$obj->loadRestrictionsFromRows( $page, $rowsToLoad );
|
||||
}
|
||||
$this->assertSame( $expected, $obj->areRestrictionsCascading( $page ) );
|
||||
}
|
||||
|
||||
public function provideAreRestrictionsCascading(): array {
|
||||
return [
|
||||
'Special page' => [ false, self::newImproperPageIdentity( NS_SPECIAL, 'X' ), null ],
|
||||
'Media page' => [ false, self::newImproperPageIdentity( NS_MEDIA, 'X' ), null ],
|
||||
'Regular page with no restrictions' =>
|
||||
[ false, PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ), [] ],
|
||||
'Regular page with restrictions' => [
|
||||
false,
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ] ],
|
||||
],
|
||||
'Regular page with cascading restrictions' => [
|
||||
true,
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[ (object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 1 ] ],
|
||||
],
|
||||
'Regular page with some cascading restrictions and some not' => [
|
||||
true,
|
||||
PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' ),
|
||||
[
|
||||
(object)[ 'pr_type' => 'edit', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 0 ],
|
||||
(object)[ 'pr_type' => 'move', 'pr_level' => 'sysop',
|
||||
'pr_expiry' => 'infinity', 'pr_cascade' => 1 ],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::flushRestrictions
|
||||
*/
|
||||
public function testFlushRestrictions(): void {
|
||||
$obj = $this->newRestrictionStore();
|
||||
$page = PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' );
|
||||
$this->assertFalse( $obj->areRestrictionsLoaded( $page ) );
|
||||
$obj->loadRestrictionsFromRows( $page, [] );
|
||||
$this->assertTrue( $obj->areRestrictionsLoaded( $page ) );
|
||||
$obj->flushRestrictions( $page );
|
||||
$this->assertFalse( $obj->areRestrictionsLoaded( $page ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getCascadeProtectionSources
|
||||
* @covers ::getCascadeProtectionSourcesInternal
|
||||
*/
|
||||
public function testGetCascadeProtectionSources() {
|
||||
$obj = $this->newRestrictionStore( [ 'db' => [ DB_REPLICA => [ 'select' =>
|
||||
static function () {
|
||||
return [
|
||||
(object)[ 'pr_page' => 1, 'page_namespace' => NS_MAIN, 'page_title' => 'test',
|
||||
'pr_expiry' => 'infinity', 'pr_type' => 'edit', 'pr_level' => 'Sysop' ]
|
||||
];
|
||||
}
|
||||
] ] ] );
|
||||
|
||||
$page = PageIdentityValue::localIdentity( 1, NS_MAIN, 'X' );
|
||||
[ $sources, $restrictions ] = $obj->getCascadeProtectionSources( $page );
|
||||
$this->assertCount( 1, $sources );
|
||||
$this->assertArrayHasKey( 'edit', $restrictions );
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue