MediaWiki 1.42.1
RestrictionStore.php
Go to the documentation of this file.
1<?php
2
4
20use stdClass;
25
32
34 public const CONSTRUCTOR_OPTIONS = [
39 ];
40
42 private $options;
43
45 private $wanCache;
46
48 private $loadBalancer;
49
51 private $linkCache;
52
54 private $linksMigration;
55
57 private $commentStore;
58
60 private $hookContainer;
61
63 private $hookRunner;
64
66 private $pageStore;
67
79 private $cache = [];
80
91 public function __construct(
92 ServiceOptions $options,
93 WANObjectCache $wanCache,
94 ILoadBalancer $loadBalancer,
95 LinkCache $linkCache,
96 LinksMigration $linksMigration,
97 CommentStore $commentStore,
98 HookContainer $hookContainer,
99 PageStore $pageStore
100 ) {
101 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
102 $this->options = $options;
103 $this->wanCache = $wanCache;
104 $this->loadBalancer = $loadBalancer;
105 $this->linkCache = $linkCache;
106 $this->linksMigration = $linksMigration;
107 $this->commentStore = $commentStore;
108 $this->hookContainer = $hookContainer;
109 $this->hookRunner = new HookRunner( $hookContainer );
110 $this->pageStore = $pageStore;
111 }
112
124 public function getRestrictions( PageIdentity $page, string $action ): array {
125 $page->assertWiki( PageIdentity::LOCAL );
126
127 $restrictions = $this->getAllRestrictions( $page );
128 return $restrictions[$action] ?? [];
129 }
130
138 public function getAllRestrictions( PageIdentity $page ): array {
139 $page->assertWiki( PageIdentity::LOCAL );
140
141 if ( !$this->areRestrictionsLoaded( $page ) ) {
142 $this->loadRestrictions( $page );
143 }
144 return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['restrictions'] ?? [];
145 }
146
155 public function getRestrictionExpiry( PageIdentity $page, string $action ): ?string {
156 $page->assertWiki( PageIdentity::LOCAL );
157
158 if ( !$this->areRestrictionsLoaded( $page ) ) {
159 $this->loadRestrictions( $page );
160 }
161 return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['expiry'][$action] ?? null;
162 }
163
178 public function getCreateProtection( PageIdentity $page ): ?array {
179 $page->assertWiki( PageIdentity::LOCAL );
180
181 $protection = $this->getCreateProtectionInternal( $page );
182 // TODO: the remapping below probably need to be migrated into other method one day
183 if ( $protection ) {
184 if ( $protection['permission'] == 'sysop' ) {
185 $protection['permission'] = 'editprotected'; // B/C
186 }
187 if ( $protection['permission'] == 'autoconfirmed' ) {
188 $protection['permission'] = 'editsemiprotected'; // B/C
189 }
190 }
191 return $protection;
192 }
193
200 public function deleteCreateProtection( PageIdentity $page ): void {
201 $page->assertWiki( PageIdentity::LOCAL );
202
203 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
204 $dbw->newDeleteQueryBuilder()
205 ->deleteFrom( 'protected_titles' )
206 ->where( [ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ] )
207 ->caller( __METHOD__ )->execute();
208 $this->cache[CacheKeyHelper::getKeyForPage( $page )]['create_protection'] = null;
209 }
210
219 public function isSemiProtected( PageIdentity $page, string $action = 'edit' ): bool {
220 $page->assertWiki( PageIdentity::LOCAL );
221
222 $restrictions = $this->getRestrictions( $page, $action );
223 $semi = $this->options->get( MainConfigNames::SemiprotectedRestrictionLevels );
224 if ( !$restrictions || !$semi ) {
225 // Not protected, or all protection is full protection
226 return false;
227 }
228
229 // Remap autoconfirmed to editsemiprotected for BC
230 foreach ( array_keys( $semi, 'editsemiprotected' ) as $key ) {
231 $semi[$key] = 'autoconfirmed';
232 }
233 foreach ( array_keys( $restrictions, 'editsemiprotected' ) as $key ) {
234 $restrictions[$key] = 'autoconfirmed';
235 }
236
237 return !array_diff( $restrictions, $semi );
238 }
239
247 public function isProtected( PageIdentity $page, string $action = '' ): bool {
248 $page->assertWiki( PageIdentity::LOCAL );
249
250 // Special pages have inherent protection (TODO: remove after switch to ProperPageIdentity)
251 if ( $page->getNamespace() === NS_SPECIAL ) {
252 return true;
253 }
254
255 // Check regular protection levels
256 $applicableTypes = $this->listApplicableRestrictionTypes( $page );
257
258 if ( $action === '' ) {
259 foreach ( $applicableTypes as $type ) {
260 if ( $this->isProtected( $page, $type ) ) {
261 return true;
262 }
263 }
264 return false;
265 }
266
267 if ( !in_array( $action, $applicableTypes ) ) {
268 return false;
269 }
270
271 return (bool)array_diff(
272 array_intersect(
273 $this->getRestrictions( $page, $action ),
274 $this->options->get( MainConfigNames::RestrictionLevels )
275 ),
276 [ '' ]
277 );
278 }
279
286 public function isCascadeProtected( PageIdentity $page ): bool {
287 $page->assertWiki( PageIdentity::LOCAL );
288
289 return $this->getCascadeProtectionSourcesInternal( $page, true );
290 }
291
298 public function listApplicableRestrictionTypes( PageIdentity $page ): array {
299 $page->assertWiki( PageIdentity::LOCAL );
300
301 if ( !$page->canExist() ) {
302 return [];
303 }
304
305 $types = $this->listAllRestrictionTypes( $page->exists() );
306
307 if ( $page->getNamespace() !== NS_FILE ) {
308 // Remove the upload restriction for non-file titles
309 $types = array_values( array_diff( $types, [ 'upload' ] ) );
310 }
311
312 if ( $this->hookContainer->isRegistered( 'TitleGetRestrictionTypes' ) ) {
313 $this->hookRunner->onTitleGetRestrictionTypes(
314 Title::newFromPageIdentity( $page ), $types );
315 }
316
317 return $types;
318 }
319
327 public function listAllRestrictionTypes( bool $exists = true ): array {
328 $types = $this->options->get( MainConfigNames::RestrictionTypes );
329 if ( $exists ) {
330 // Remove the create restriction for existing titles
331 return array_values( array_diff( $types, [ 'create' ] ) );
332 }
333
334 // Only the create restrictions apply to non-existing titles
335 return array_values( array_intersect( $types, [ 'create' ] ) );
336 }
337
346 public function loadRestrictions(
347 PageIdentity $page, int $flags = IDBAccessObject::READ_NORMAL
348 ): void {
349 $page->assertWiki( PageIdentity::LOCAL );
350
351 if ( !$page->canExist() ) {
352 return;
353 }
354
355 $readLatest = DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST );
356
357 if ( $this->areRestrictionsLoaded( $page ) && !$readLatest ) {
358 return;
359 }
360
361 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
362
363 $cacheEntry['restrictions'] = [];
364
365 // XXX Work around https://phabricator.wikimedia.org/T287575
366 if ( $readLatest ) {
367 $page = $this->pageStore->getPageByReference( $page, $flags ) ?? $page;
368 }
369 $id = $page->getId();
370 if ( $id ) {
371 $fname = __METHOD__;
372 $loadRestrictionsFromDb = static function ( IReadableDatabase $dbr ) use ( $fname, $id ) {
373 return iterator_to_array(
374 $dbr->newSelectQueryBuilder()
375 ->select( [ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ] )
376 ->from( 'page_restrictions' )
377 ->where( [ 'pr_page' => $id ] )
378 ->caller( $fname )->fetchResultSet()
379 );
380 };
381
382 if ( $readLatest ) {
383 $dbr = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
384 $rows = $loadRestrictionsFromDb( $dbr );
385 } else {
386 $this->linkCache->addLinkObj( $page );
387 $latestRev = $this->linkCache->getGoodLinkFieldObj( $page, 'revision' );
388 if ( !$latestRev ) {
389 // This method can get called in the middle of page creation
390 // (WikiPage::doUserEditContent) where a page might have an
391 // id but no revisions, while checking the "autopatrol" permission.
392 $rows = [];
393 } else {
394 $rows = $this->wanCache->getWithSetCallback(
395 // Page protections always leave a new null revision
396 $this->wanCache->makeKey( 'page-restrictions', 'v1', $id, $latestRev ),
397 $this->wanCache::TTL_DAY,
398 function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) {
399 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
400 $setOpts += Database::getCacheSetOptions( $dbr );
401 if ( $this->loadBalancer->hasOrMadeRecentPrimaryChanges() ) {
402 // TODO: cleanup Title cache and caller assumption mess in general
403 $ttl = WANObjectCache::TTL_UNCACHEABLE;
404 }
405
406 return $loadRestrictionsFromDb( $dbr );
407 }
408 );
409 }
410 }
411
412 $this->loadRestrictionsFromRows( $page, $rows );
413 } else {
414 $titleProtection = $this->getCreateProtectionInternal( $page );
415
416 if ( $titleProtection ) {
417 $now = wfTimestampNow();
418 $expiry = $titleProtection['expiry'];
419
420 if ( !$expiry || $expiry > $now ) {
421 // Apply the restrictions
422 $cacheEntry['expiry']['create'] = $expiry ?: null;
423 $cacheEntry['restrictions']['create'] =
424 explode( ',', trim( $titleProtection['permission'] ) );
425 } else {
426 // Get rid of the old restrictions
427 $cacheEntry['create_protection'] = null;
428 }
429 } else {
430 $cacheEntry['expiry']['create'] = 'infinity';
431 }
432 }
433 }
434
443 PageIdentity $page, array $rows
444 ): void {
445 $page->assertWiki( PageIdentity::LOCAL );
446
447 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
448
449 $restrictionTypes = $this->listApplicableRestrictionTypes( $page );
450
451 foreach ( $restrictionTypes as $type ) {
452 $cacheEntry['restrictions'][$type] = [];
453 $cacheEntry['expiry'][$type] = 'infinity';
454 }
455
456 $cacheEntry['cascade'] = false;
457
458 if ( !$rows ) {
459 return;
460 }
461
462 // New restriction format -- load second to make them override old-style restrictions.
463 $now = wfTimestampNow();
464
465 // Cycle through all the restrictions.
466 foreach ( $rows as $row ) {
467 // Don't take care of restrictions types that aren't allowed
468 if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
469 continue;
470 }
471
472 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
473 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
474
475 // Only apply the restrictions if they haven't expired!
476 // XXX Why would !$expiry ever be true? It should always be either 'infinity' or a
477 // string consisting of 14 digits. Likewise for the ?: below.
478 if ( !$expiry || $expiry > $now ) {
479 $cacheEntry['expiry'][$row->pr_type] = $expiry ?: null;
480 $cacheEntry['restrictions'][$row->pr_type]
481 = explode( ',', trim( $row->pr_level ) );
482 if ( $row->pr_cascade ) {
483 $cacheEntry['cascade'] = true;
484 }
485 }
486 }
487 }
488
499 private function getCreateProtectionInternal( PageIdentity $page ): ?array {
500 // Can't protect pages in special namespaces
501 if ( !$page->canExist() ) {
502 return null;
503 }
504
505 // Can't apply this type of protection to pages that exist.
506 if ( $page->exists() ) {
507 return null;
508 }
509
510 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
511
512 if ( !$cacheEntry || !array_key_exists( 'create_protection', $cacheEntry ) ) {
513 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
514 $commentQuery = $this->commentStore->getJoin( 'pt_reason' );
515 $row = $dbr->selectRow(
516 [ 'protected_titles' ] + $commentQuery['tables'],
517 [ 'pt_user', 'pt_expiry', 'pt_create_perm' ] + $commentQuery['fields'],
518 [ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ],
519 __METHOD__,
520 [],
521 $commentQuery['joins']
522 );
523
524 if ( $row ) {
525 $cacheEntry['create_protection'] = [
526 'user' => $row->pt_user,
527 'expiry' => $dbr->decodeExpiry( $row->pt_expiry ),
528 'permission' => $row->pt_create_perm,
529 'reason' => $this->commentStore->getComment( 'pt_reason', $row )->text,
530 ];
531 } else {
532 $cacheEntry['create_protection'] = null;
533 }
534
535 }
536
537 return $cacheEntry['create_protection'];
538 }
539
548 public function getCascadeProtectionSources( PageIdentity $page ): array {
549 $page->assertWiki( PageIdentity::LOCAL );
550
551 return $this->getCascadeProtectionSourcesInternal( $page, false );
552 }
553
562 private function getCascadeProtectionSourcesInternal(
563 PageIdentity $page, bool $shortCircuit = false
564 ) {
565 if ( !$page->canExist() ) {
566 return $shortCircuit ? false : [ [], [] ];
567 }
568
569 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
570
571 if ( !$shortCircuit && isset( $cacheEntry['cascade_sources'] ) ) {
572 return $cacheEntry['cascade_sources'];
573 } elseif ( $shortCircuit && isset( $cacheEntry['has_cascading'] ) ) {
574 return $cacheEntry['has_cascading'];
575 }
576
577 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
578 $queryBuilder = $dbr->newSelectQueryBuilder();
579 $queryBuilder->select( [ 'pr_expiry' ] )
580 ->from( 'page_restrictions' )
581 ->where( [ 'pr_cascade' => 1 ] );
582
583 if ( $page->getNamespace() === NS_FILE ) {
584 // Files transclusion may receive cascading protection in the future
585 // see https://phabricator.wikimedia.org/T241453
586 $queryBuilder->join( 'imagelinks', null, 'il_from=pr_page' );
587 $queryBuilder->andWhere( [ 'il_to' => $page->getDBkey() ] );
588 } else {
589 $queryBuilder->join( 'templatelinks', null, 'tl_from=pr_page' );
590 $queryBuilder->andWhere(
591 $this->linksMigration->getLinksConditions(
592 'templatelinks',
593 TitleValue::newFromPage( $page )
594 )
595 );
596 }
597
598 if ( !$shortCircuit ) {
599 $queryBuilder->fields( [ 'pr_page', 'page_namespace', 'page_title', 'pr_type', 'pr_level' ] );
600 $queryBuilder->join( 'page', null, 'page_id=pr_page' );
601 }
602
603 $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
604
605 $sources = [];
606 $pageRestrictions = [];
607 $now = wfTimestampNow();
608
609 foreach ( $res as $row ) {
610 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
611 if ( $expiry > $now ) {
612 if ( $shortCircuit ) {
613 $cacheEntry['has_cascading'] = true;
614 return true;
615 }
616
617 $sources[$row->pr_page] = new PageIdentityValue( $row->pr_page,
618 $row->page_namespace, $row->page_title, PageIdentity::LOCAL );
619 // Add groups needed for each restriction type if its not already there
620 // Make sure this restriction type still exists
621
622 if ( !isset( $pageRestrictions[$row->pr_type] ) ) {
623 $pageRestrictions[$row->pr_type] = [];
624 }
625
626 if ( !in_array( $row->pr_level, $pageRestrictions[$row->pr_type] ) ) {
627 $pageRestrictions[$row->pr_type][] = $row->pr_level;
628 }
629 }
630 }
631
632 $cacheEntry['has_cascading'] = (bool)$sources;
633
634 if ( $shortCircuit ) {
635 return false;
636 }
637
638 $cacheEntry['cascade_sources'] = [ $sources, $pageRestrictions ];
639 return [ $sources, $pageRestrictions ];
640 }
641
647 public function areRestrictionsLoaded( PageIdentity $page ): bool {
648 $page->assertWiki( PageIdentity::LOCAL );
649
650 return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['restrictions'] );
651 }
652
659 public function areCascadeProtectionSourcesLoaded( PageIdentity $page ): bool {
660 $page->assertWiki( PageIdentity::LOCAL );
661
662 return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade_sources'] );
663 }
664
671 public function areRestrictionsCascading( PageIdentity $page ): bool {
672 $page->assertWiki( PageIdentity::LOCAL );
673
674 if ( !$this->areRestrictionsLoaded( $page ) ) {
675 $this->loadRestrictions( $page );
676 }
677 return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade'] ?? false;
678 }
679
687 public function flushRestrictions( PageIdentity $page ): void {
688 $page->assertWiki( PageIdentity::LOCAL );
689
690 unset( $this->cache[CacheKeyHelper::getKeyForPage( $page )] );
691 }
692
693}
const NS_FILE
Definition Defines.php:70
const NS_SPECIAL
Definition Defines.php:53
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Helper class for DAO classes.
Helper class for mapping value objects representing basic entities to cache keys.
Cache for article titles (prefixed DB keys) and ids linked from one source.
Definition LinkCache.php:53
Handle database storage of comments such as edit summaries and log reasons.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Service for compat reading of links tables.
A class containing constants representing the names of configuration variables.
const NamespaceProtection
Name constant for the NamespaceProtection setting, for use with Config::get()
const RestrictionTypes
Name constant for the RestrictionTypes setting, for use with Config::get()
const SemiprotectedRestrictionLevels
Name constant for the SemiprotectedRestrictionLevels setting, for use with Config::get()
const RestrictionLevels
Name constant for the RestrictionLevels setting, for use with Config::get()
Immutable value object representing a page identity.
loadRestrictionsFromRows(PageIdentity $page, array $rows)
Compiles list of active page restrictions for this existing page.
getAllRestrictions(PageIdentity $page)
Returns the restricted actions and their restrictions for the specified page.
listAllRestrictionTypes(bool $exists=true)
Get a filtered list of all restriction types supported by this wiki.
getRestrictions(PageIdentity $page, string $action)
Returns list of restrictions for specified page.
deleteCreateProtection(PageIdentity $page)
Remove any title creation protection due to page existing.
getCascadeProtectionSources(PageIdentity $page)
Cascading protection: Get the source of any cascading restrictions on this page.
getRestrictionExpiry(PageIdentity $page, string $action)
Get the expiry time for the restriction against a given action.
isCascadeProtected(PageIdentity $page)
Cascading protection: Return true if cascading restrictions apply to this page, false if not.
isSemiProtected(PageIdentity $page, string $action='edit')
Is this page "semi-protected" - the only protection levels are listed in $wgSemiprotectedRestrictionL...
__construct(ServiceOptions $options, WANObjectCache $wanCache, ILoadBalancer $loadBalancer, LinkCache $linkCache, LinksMigration $linksMigration, CommentStore $commentStore, HookContainer $hookContainer, PageStore $pageStore)
listApplicableRestrictionTypes(PageIdentity $page)
Returns restriction types for the current page.
isProtected(PageIdentity $page, string $action='')
Does the title correspond to a protected article?
flushRestrictions(PageIdentity $page)
Flush the protection cache in this object and force reload from the database.
areRestrictionsCascading(PageIdentity $page)
Checks if restrictions are cascading for the current page.
loadRestrictions(PageIdentity $page, int $flags=IDBAccessObject::READ_NORMAL)
Load restrictions from page.page_restrictions and the page_restrictions table.
getCreateProtection(PageIdentity $page)
Is this title subject to protection against creation?
areCascadeProtectionSourcesLoaded(PageIdentity $page)
Determines whether cascading protection sources have already been loaded from the database.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:78
Multi-datacenter aware caching interface.
Interface for database access objects.
Interface for objects (potentially) representing an editable wiki page.
getId( $wikiId=self::LOCAL)
Returns the page ID.
canExist()
Checks whether this PageIdentity represents a "proper" page, meaning that it could exist as an editab...
exists()
Checks if the page currently exists.
getNamespace()
Returns the page's namespace number.
getDBkey()
Get the page title in DB key form.
This class is a delegate to ILBFactory for a given database cluster.
A database connection without write operations.
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28