MediaWiki REL1_39
RestrictionStore.php
Go to the documentation of this file.
1<?php
2
4
8use LinkCache;
18use stdClass;
19use Title;
20use TitleValue;
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
156 public function getRestrictionExpiry( PageIdentity $page, string $action ): ?string {
157 $page->assertWiki( PageIdentity::LOCAL );
158
159 if ( !$this->areRestrictionsLoaded( $page ) ) {
160 $this->loadRestrictions( $page );
161 }
162 return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['expiry'][$action] ?? null;
163 }
164
179 public function getCreateProtection( PageIdentity $page ): ?array {
180 $page->assertWiki( PageIdentity::LOCAL );
181
182 $protection = $this->getCreateProtectionInternal( $page );
183 // TODO: the remapping below probably need to be migrated into other method one day
184 if ( $protection ) {
185 if ( $protection['permission'] == 'sysop' ) {
186 $protection['permission'] = 'editprotected'; // B/C
187 }
188 if ( $protection['permission'] == 'autoconfirmed' ) {
189 $protection['permission'] = 'editsemiprotected'; // B/C
190 }
191 }
192 return $protection;
193 }
194
201 public function deleteCreateProtection( PageIdentity $page ): void {
202 $page->assertWiki( PageIdentity::LOCAL );
203
204 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
205 $dbw->delete(
206 'protected_titles',
207 [ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ],
208 __METHOD__
209 );
210 $this->cache[CacheKeyHelper::getKeyForPage( $page )]['create_protection'] = null;
211 }
212
221 public function isSemiProtected( PageIdentity $page, string $action = 'edit' ): bool {
222 $page->assertWiki( PageIdentity::LOCAL );
223
224 $restrictions = $this->getRestrictions( $page, $action );
225 $semi = $this->options->get( MainConfigNames::SemiprotectedRestrictionLevels );
226 if ( !$restrictions || !$semi ) {
227 // Not protected, or all protection is full protection
228 return false;
229 }
230
231 // Remap autoconfirmed to editsemiprotected for BC
232 foreach ( array_keys( $semi, 'editsemiprotected' ) as $key ) {
233 $semi[$key] = 'autoconfirmed';
234 }
235 foreach ( array_keys( $restrictions, 'editsemiprotected' ) as $key ) {
236 $restrictions[$key] = 'autoconfirmed';
237 }
238
239 return !array_diff( $restrictions, $semi );
240 }
241
249 public function isProtected( PageIdentity $page, string $action = '' ): bool {
250 $page->assertWiki( PageIdentity::LOCAL );
251
252 // Special pages have inherent protection (TODO: remove after switch to ProperPageIdentity)
253 if ( $page->getNamespace() === NS_SPECIAL ) {
254 return true;
255 }
256
257 // Check regular protection levels
258 $applicableTypes = $this->listApplicableRestrictionTypes( $page );
259
260 if ( $action === '' ) {
261 foreach ( $applicableTypes as $type ) {
262 if ( $this->isProtected( $page, $type ) ) {
263 return true;
264 }
265 }
266 return false;
267 }
268
269 if ( !in_array( $action, $applicableTypes ) ) {
270 return false;
271 }
272
273 return (bool)array_diff(
274 array_intersect(
275 $this->getRestrictions( $page, $action ),
276 $this->options->get( MainConfigNames::RestrictionLevels )
277 ),
278 [ '' ]
279 );
280 }
281
288 public function isCascadeProtected( PageIdentity $page ): bool {
289 $page->assertWiki( PageIdentity::LOCAL );
290
291 return $this->getCascadeProtectionSourcesInternal( $page, true );
292 }
293
300 public function listApplicableRestrictionTypes( PageIdentity $page ): array {
301 $page->assertWiki( PageIdentity::LOCAL );
302
303 if ( !$page->canExist() ) {
304 return [];
305 }
306
307 $types = $this->listAllRestrictionTypes( $page->exists() );
308
309 if ( $page->getNamespace() !== NS_FILE ) {
310 // Remove the upload restriction for non-file titles
311 $types = array_values( array_diff( $types, [ 'upload' ] ) );
312 }
313
314 if ( $this->hookContainer->isRegistered( 'TitleGetRestrictionTypes' ) ) {
315 $this->hookRunner->onTitleGetRestrictionTypes(
316 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
317 Title::castFromPageIdentity( $page ), $types );
318 }
319
320 return $types;
321 }
322
330 public function listAllRestrictionTypes( bool $exists = true ): array {
331 $types = $this->options->get( MainConfigNames::RestrictionTypes );
332 if ( $exists ) {
333 // Remove the create restriction for existing titles
334 return array_values( array_diff( $types, [ 'create' ] ) );
335 }
336
337 // Only the create and upload restrictions apply to non-existing titles
338 return array_values( array_intersect( $types, [ 'create', 'upload' ] ) );
339 }
340
349 public function loadRestrictions(
350 PageIdentity $page, int $flags = IDBAccessObject::READ_NORMAL
351 ): void {
352 $page->assertWiki( PageIdentity::LOCAL );
353
354 if ( !$page->canExist() ) {
355 return;
356 }
357
358 $readLatest = DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST );
359
360 if ( $this->areRestrictionsLoaded( $page ) && !$readLatest ) {
361 return;
362 }
363
364 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
365
366 $cacheEntry['restrictions'] = [];
367
368 // XXX Work around https://phabricator.wikimedia.org/T287575
369 if ( $readLatest ) {
370 $page = $this->pageStore->getPageByReference( $page, $flags ) ?? $page;
371 }
372 $id = $page->getId();
373 if ( $id ) {
374 $fname = __METHOD__;
375 $loadRestrictionsFromDb = static function ( IDatabase $dbr ) use ( $fname, $id ) {
376 return iterator_to_array(
377 $dbr->select(
378 'page_restrictions',
379 [ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
380 [ 'pr_page' => $id ],
381 $fname
382 )
383 );
384 };
385
386 if ( $readLatest ) {
387 $dbr = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
388 $rows = $loadRestrictionsFromDb( $dbr );
389 } else {
390 $this->linkCache->addLinkObj( $page );
391 $latestRev = $this->linkCache->getGoodLinkFieldObj( $page, 'revision' );
392 if ( !$latestRev ) {
393 // This method can get called in the middle of page creation
394 // (WikiPage::doUserEditContent) where a page might have an
395 // id but no revisions, while checking the "autopatrol" permission.
396 $rows = [];
397 } else {
398 $rows = $this->wanCache->getWithSetCallback(
399 // Page protections always leave a new null revision
400 $this->wanCache->makeKey( 'page-restrictions', 'v1', $id, $latestRev ),
401 $this->wanCache::TTL_DAY,
402 function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) {
403 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
404 $setOpts += Database::getCacheSetOptions( $dbr );
405 if ( $this->loadBalancer->hasOrMadeRecentPrimaryChanges() ) {
406 // TODO: cleanup Title cache and caller assumption mess in general
407 $ttl = WANObjectCache::TTL_UNCACHEABLE;
408 }
409
410 return $loadRestrictionsFromDb( $dbr );
411 }
412 );
413 }
414 }
415
416 $this->loadRestrictionsFromRows( $page, $rows );
417 } else {
418 $titleProtection = $this->getCreateProtectionInternal( $page );
419
420 if ( $titleProtection ) {
421 $now = wfTimestampNow();
422 $expiry = $titleProtection['expiry'];
423
424 if ( !$expiry || $expiry > $now ) {
425 // Apply the restrictions
426 $cacheEntry['expiry']['create'] = $expiry ?: null;
427 $cacheEntry['restrictions']['create'] =
428 explode( ',', trim( $titleProtection['permission'] ) );
429 } else {
430 // Get rid of the old restrictions
431 $cacheEntry['create_protection'] = null;
432 }
433 } else {
434 $cacheEntry['expiry']['create'] = 'infinity';
435 }
436 }
437 }
438
447 PageIdentity $page, array $rows
448 ): void {
449 $page->assertWiki( PageIdentity::LOCAL );
450
451 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
452
453 $restrictionTypes = $this->listApplicableRestrictionTypes( $page );
454
455 foreach ( $restrictionTypes as $type ) {
456 $cacheEntry['restrictions'][$type] = [];
457 $cacheEntry['expiry'][$type] = 'infinity';
458 }
459
460 $cacheEntry['cascade'] = false;
461
462 if ( !$rows ) {
463 return;
464 }
465
466 // New restriction format -- load second to make them override old-style restrictions.
467 $now = wfTimestampNow();
468
469 // Cycle through all the restrictions.
470 foreach ( $rows as $row ) {
471 // Don't take care of restrictions types that aren't allowed
472 if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
473 continue;
474 }
475
476 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
477 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
478
479 // Only apply the restrictions if they haven't expired!
480 // XXX Why would !$expiry ever be true? It should always be either 'infinity' or a
481 // string consisting of 14 digits. Likewise for the ?: below.
482 if ( !$expiry || $expiry > $now ) {
483 $cacheEntry['expiry'][$row->pr_type] = $expiry ?: null;
484 $cacheEntry['restrictions'][$row->pr_type]
485 = explode( ',', trim( $row->pr_level ) );
486 if ( $row->pr_cascade ) {
487 $cacheEntry['cascade'] = true;
488 }
489 }
490 }
491 }
492
503 private function getCreateProtectionInternal( PageIdentity $page ): ?array {
504 // Can't protect pages in special namespaces
505 if ( !$page->canExist() ) {
506 return null;
507 }
508
509 // Can't apply this type of protection to pages that exist.
510 if ( $page->exists() ) {
511 return null;
512 }
513
514 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
515
516 if ( !$cacheEntry || !array_key_exists( 'create_protection', $cacheEntry ) ) {
517 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
518 $commentQuery = $this->commentStore->getJoin( 'pt_reason' );
519 $row = $dbr->selectRow(
520 [ 'protected_titles' ] + $commentQuery['tables'],
521 [ 'pt_user', 'pt_expiry', 'pt_create_perm' ] + $commentQuery['fields'],
522 [ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ],
523 __METHOD__,
524 [],
525 $commentQuery['joins']
526 );
527
528 if ( $row ) {
529 $cacheEntry['create_protection'] = [
530 'user' => $row->pt_user,
531 'expiry' => $dbr->decodeExpiry( $row->pt_expiry ),
532 'permission' => $row->pt_create_perm,
533 'reason' => $this->commentStore->getComment( 'pt_reason', $row )->text,
534 ];
535 } else {
536 $cacheEntry['create_protection'] = null;
537 }
538
539 }
540
541 return $cacheEntry['create_protection'];
542 }
543
554 public function getCascadeProtectionSources( PageIdentity $page ): array {
555 $page->assertWiki( PageIdentity::LOCAL );
556
557 return $this->getCascadeProtectionSourcesInternal( $page, false );
558 }
559
568 private function getCascadeProtectionSourcesInternal(
569 PageIdentity $page, bool $shortCircuit = false
570 ) {
571 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
572
573 if ( !$shortCircuit && isset( $cacheEntry['cascade_sources'] ) ) {
574 return $cacheEntry['cascade_sources'];
575 } elseif ( $shortCircuit && isset( $cacheEntry['has_cascading'] ) ) {
576 return $cacheEntry['has_cascading'];
577 }
578
579 if ( $page->getNamespace() === NS_FILE ) {
580 // Files transclusion may receive cascading protection in the future
581 // see https://phabricator.wikimedia.org/T241453
582 $tables = [ 'imagelinks', 'page_restrictions' ];
583 $where_clauses = [
584 'il_to' => $page->getDBkey(),
585 'il_from=pr_page',
586 'pr_cascade' => 1
587 ];
588 } else {
589 $tables = [ 'templatelinks', 'page_restrictions' ];
590 $where_clauses = $this->linksMigration->getLinksConditions(
591 'templatelinks',
592 TitleValue::newFromPage( $page )
593 );
594 $where_clauses[] = 'tl_from=pr_page';
595 $where_clauses['pr_cascade'] = 1;
596 }
597
598 if ( $shortCircuit ) {
599 $cols = [ 'pr_expiry' ];
600 } else {
601 $cols = [ 'pr_page', 'page_namespace', 'page_title',
602 'pr_expiry', 'pr_type', 'pr_level' ];
603 $where_clauses[] = 'page_id=pr_page';
604 $tables[] = 'page';
605 }
606
607 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
608 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
609
610 $sources = [];
611 $pageRestrictions = [];
612 $now = wfTimestampNow();
613
614 foreach ( $res as $row ) {
615 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
616 if ( $expiry > $now ) {
617 if ( $shortCircuit ) {
618 $cacheEntry['has_cascading'] = true;
619 return true;
620 }
621
622 $sources[$row->pr_page] = new PageIdentityValue( $row->pr_page,
623 $row->page_namespace, $row->page_title, PageIdentity::LOCAL );
624 // Add groups needed for each restriction type if its not already there
625 // Make sure this restriction type still exists
626
627 if ( !isset( $pageRestrictions[$row->pr_type] ) ) {
628 $pageRestrictions[$row->pr_type] = [];
629 }
630
631 if ( !in_array( $row->pr_level, $pageRestrictions[$row->pr_type] ) ) {
632 $pageRestrictions[$row->pr_type][] = $row->pr_level;
633 }
634 }
635 }
636
637 $cacheEntry['has_cascading'] = (bool)$sources;
638
639 if ( $shortCircuit ) {
640 return false;
641 }
642
643 $cacheEntry['cascade_sources'] = [ $sources, $pageRestrictions ];
644 return [ $sources, $pageRestrictions ];
645 }
646
652 public function areRestrictionsLoaded( PageIdentity $page ): bool {
653 $page->assertWiki( PageIdentity::LOCAL );
654
655 return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['restrictions'] );
656 }
657
664 public function areCascadeProtectionSourcesLoaded( PageIdentity $page ): bool {
665 $page->assertWiki( PageIdentity::LOCAL );
666
667 return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade_sources'] );
668 }
669
676 public function areRestrictionsCascading( PageIdentity $page ): bool {
677 $page->assertWiki( PageIdentity::LOCAL );
678
679 if ( !$this->areRestrictionsLoaded( $page ) ) {
680 $this->loadRestrictions( $page );
681 }
682 return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade'] ?? false;
683 }
684
692 public function flushRestrictions( PageIdentity $page ): void {
693 $page->assertWiki( PageIdentity::LOCAL );
694
695 unset( $this->cache[CacheKeyHelper::getKeyForPage( $page )] );
696 }
697
698}
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'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
Handle database storage of comments such as edit summaries and log reasons.
Helper class for DAO classes.
Cache for article titles (prefixed DB keys) and ids linked from one source.
Definition LinkCache.php:42
Helper class for mapping value objects representing basic entities to cache keys.
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 a page (or page fragment) title within MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:49
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.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
Create and track the database connections and transactions for a given database cluster.
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28