75 $this->options = $options;
76 $this->wanCache = $wanCache;
77 $this->loadBalancerFactory = $loadBalancerFactory;
78 $this->linkCache = $linkCache;
79 $this->linksMigration = $linksMigration;
80 $this->commentStore = $commentStore;
81 $this->hookContainer = $hookContainer;
82 $this->hookRunner =
new HookRunner( $hookContainer );
83 $this->pageStore = $pageStore;
105 if ( !in_array( $action, $restrictionTypes ) ) {
110 return $restrictions[$action] ?? [];
123 if ( !$this->areRestrictionsLoaded( $page ) ) {
124 $this->loadRestrictions( $page );
126 return $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'restrictions'] ?? [];
140 if ( !$this->areRestrictionsLoaded( $page ) ) {
141 $this->loadRestrictions( $page );
143 return $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'expiry'][$action] ??
null;
159 $protection = $this->getCreateProtectionInternal( $page );
162 if ( $protection[
'permission'] ==
'sysop' ) {
163 $protection[
'permission'] =
'editprotected';
165 if ( $protection[
'permission'] ==
'autoconfirmed' ) {
166 $protection[
'permission'] =
'editsemiprotected';
181 $dbw = $this->loadBalancerFactory->getPrimaryDatabase();
182 $dbw->newDeleteQueryBuilder()
183 ->deleteFrom(
'protected_titles' )
185 ->caller( __METHOD__ )->execute();
186 $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'create_protection'] =
null;
200 $restrictions = $this->getRestrictions( $page, $action );
201 $semi = $this->options->get( MainConfigNames::SemiprotectedRestrictionLevels );
202 if ( !$restrictions || !$semi ) {
208 foreach ( array_keys( $semi,
'editsemiprotected' ) as $key ) {
209 $semi[$key] =
'autoconfirmed';
211 foreach ( array_keys( $restrictions,
'editsemiprotected' ) as $key ) {
212 $restrictions[$key] =
'autoconfirmed';
215 return !array_diff( $restrictions, $semi );
234 $applicableTypes = $this->listApplicableRestrictionTypes( $page );
236 if ( $action ===
'' ) {
237 foreach ( $applicableTypes as $type ) {
238 if ( $this->isProtected( $page, $type ) ) {
245 if ( !in_array( $action, $applicableTypes ) ) {
249 return (
bool)array_diff(
251 $this->getRestrictions( $page, $action ),
252 $this->options->get( MainConfigNames::RestrictionLevels )
267 return $this->shouldUseVirtualDomains( $page )
268 ? $this->getCascadeProtectionSourcesInternal( $page )[0] !== []
269 : $this->getCascadeProtectionSourcesInternalJoined( $page )[0] !== [];
285 $types = $this->listAllRestrictionTypes( $page->
exists() );
289 $types = array_values( array_diff( $types, [
'upload' ] ) );
292 if ( $this->hookContainer->isRegistered(
'TitleGetRestrictionTypes' ) ) {
293 $this->hookRunner->onTitleGetRestrictionTypes(
294 Title::newFromPageIdentity( $page ), $types );
311 return array_values( array_diff( $types, [
'create' ] ) );
315 return array_values( array_intersect( $types, [
'create' ] ) );
327 PageIdentity $page,
int $flags = IDBAccessObject::READ_NORMAL
335 $readLatest = DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST );
337 if ( $this->areRestrictionsLoaded( $page ) && !$readLatest ) {
341 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
343 $cacheEntry[
'restrictions'] = [];
347 $page = $this->pageStore->getPageByReference( $page, $flags ) ?? $page;
349 $id = $page->
getId();
352 $loadRestrictionsFromDb =
static function ( IReadableDatabase $dbr ) use ( $fname, $id ) {
353 return iterator_to_array(
354 $dbr->newSelectQueryBuilder()
355 ->select( [
'pr_type',
'pr_expiry',
'pr_level',
'pr_cascade' ] )
356 ->from(
'page_restrictions' )
357 ->where( [
'pr_page' => $id ] )
358 ->caller( $fname )->fetchResultSet()
363 $dbr = $this->loadBalancerFactory->getPrimaryDatabase();
364 $rows = $loadRestrictionsFromDb( $dbr );
366 $this->pageStore->getPageForLink( TitleValue::newFromPage( $page ) )->getId();
367 $latestRev = $this->linkCache->getGoodLinkFieldObj( $page,
'revision' );
374 $rows = $this->wanCache->getWithSetCallback(
376 $this->wanCache->makeKey(
'page-restrictions',
'v1', $id, $latestRev ),
377 $this->wanCache::TTL_DAY,
378 function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) {
379 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
380 $setOpts += Database::getCacheSetOptions( $dbr );
381 if ( $this->loadBalancerFactory->hasOrMadeRecentPrimaryChanges() ) {
383 $ttl = WANObjectCache::TTL_UNCACHEABLE;
386 return $loadRestrictionsFromDb( $dbr );
392 $this->loadRestrictionsFromRows( $page, $rows );
394 $titleProtection = $this->getCreateProtectionInternal( $page );
396 if ( $titleProtection ) {
398 $expiry = $titleProtection[
'expiry'];
400 if ( !$expiry || $expiry > $now ) {
402 $cacheEntry[
'expiry'][
'create'] = $expiry ?:
null;
403 $cacheEntry[
'restrictions'][
'create'] =
404 explode(
',', trim( $titleProtection[
'permission'] ) );
407 $cacheEntry[
'create_protection'] =
null;
410 $cacheEntry[
'expiry'][
'create'] =
'infinity';
427 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
429 $restrictionTypes = $this->listApplicableRestrictionTypes( $page );
431 foreach ( $restrictionTypes as $type ) {
432 $cacheEntry[
'restrictions'][$type] = [];
433 $cacheEntry[
'expiry'][$type] =
'infinity';
436 $cacheEntry[
'cascade'] =
false;
446 foreach ( $rows as $row ) {
448 if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
452 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
453 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
458 if ( !$expiry || $expiry > $now ) {
459 $cacheEntry[
'expiry'][$row->pr_type] = $expiry ?:
null;
460 $cacheEntry[
'restrictions'][$row->pr_type]
461 = explode(
',', trim( $row->pr_level ) );
462 if ( $row->pr_cascade ) {
463 $cacheEntry[
'cascade'] =
true;
479 private function getCreateProtectionInternal( PageIdentity $page ): ?array {
481 if ( !$page->canExist() ) {
490 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
492 if ( !$cacheEntry || !array_key_exists(
'create_protection', $cacheEntry ) ) {
493 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
494 $commentQuery = $this->commentStore->getJoin(
'pt_reason' );
495 $row = $dbr->newSelectQueryBuilder()
496 ->select( [
'pt_user',
'pt_expiry',
'pt_create_perm' ] )
497 ->from(
'protected_titles' )
499 ->queryInfo( $commentQuery )
500 ->caller( __METHOD__ )
504 $cacheEntry[
'create_protection'] = [
505 'user' => $row->pt_user,
506 'expiry' => $dbr->decodeExpiry( $row->pt_expiry ),
507 'permission' => $row->pt_create_perm,
508 'reason' => $this->commentStore->getComment(
'pt_reason', $row )->text,
511 $cacheEntry[
'create_protection'] =
null;
516 return $cacheEntry[
'create_protection'];
534 return $this->shouldUseVirtualDomains( $page )
535 ? $this->getCascadeProtectionSourcesInternal( $page )
536 : $this->getCascadeProtectionSourcesInternalJoined( $page );
550 private function getCascadeProtectionSourcesInternal(
553 if ( !$page->canExist() ) {
554 return [ [], [], [], [] ];
557 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
559 if ( isset( $cacheEntry[
'cascade_sources'] ) ) {
560 return $cacheEntry[
'cascade_sources'];
563 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
566 $cascadeRestrictions = $dbr->newSelectQueryBuilder()
575 ->from(
'page_restrictions' )
576 ->join(
'page',
null,
'page_id=pr_page' )
577 ->where( [
'pr_cascade' => 1 ] )
578 ->caller( __METHOD__ )
581 if ( $cascadeRestrictions->numRows() === 0 ) {
582 return [ [], [], [], [] ];
585 $restrictionsByPage = [];
586 foreach ( $cascadeRestrictions as $row ) {
587 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
588 if ( $expiry > $now ) {
589 if ( !isset( $restrictionsByPage[$row->pr_page] ) ) {
590 $restrictionsByPage[$row->pr_page] = [
591 'title' => PageIdentityValue::localIdentity(
593 (
int)$row->page_namespace,
597 $row->pr_type => $row->pr_level
601 $restrictionsByPage[$row->pr_page][
'restrictions'][$row->pr_type] = $row->pr_level;
606 if ( $restrictionsByPage === [] ) {
607 return [ [], [], [], [] ];
610 $title = TitleValue::newFromPage( $page );
612 $templateLinksDb = $this->loadBalancerFactory->getReplicaDatabase( TemplateLinksTable::VIRTUAL_DOMAIN );
613 $templateLinks = $templateLinksDb->newSelectQueryBuilder()
614 ->select(
'tl_from' )
615 ->from(
'templatelinks' )
616 ->where( [
'tl_from' => array_keys( $restrictionsByPage ) ] )
617 ->andWhere( $this->linksMigration->getLinksConditions(
'templatelinks', $title ) )
618 ->caller( __METHOD__ )
623 $pageRestrictions = [];
625 foreach ( $templateLinks as $link ) {
626 $pageData = $restrictionsByPage[$link->tl_from];
627 $tlSources[$link->tl_from] = $pageData[
'title'];
628 foreach ( $pageData[
'restrictions'] as $type => $level ) {
629 if ( !isset( $pageRestrictions[$type] ) ) {
630 $pageRestrictions[$type] = [];
633 if ( !in_array( $level, $pageRestrictions[$type] ) ) {
634 $pageRestrictions[$type][] = $level;
640 $imageLinksDb = $this->loadBalancerFactory->getReplicaDatabase( ImageLinksTable::VIRTUAL_DOMAIN );
641 $imageLinks = $imageLinksDb->newSelectQueryBuilder()
642 ->select(
'il_from' )
643 ->from(
'imagelinks' )
644 ->where( [
'il_from' => array_keys( $restrictionsByPage ) ] )
645 ->andWhere( $this->linksMigration->getLinksConditions(
'imagelinks', $title ) )
646 ->caller( __METHOD__ )
649 foreach ( $imageLinks as $link ) {
650 $pageData = $restrictionsByPage[$link->il_from];
651 $ilSources[$link->il_from] = $pageData[
'title'];
652 foreach ( $pageData[
'restrictions'] as $type => $level ) {
653 if ( !isset( $pageRestrictions[$type] ) ) {
654 $pageRestrictions[$type] = [];
657 if ( !in_array( $level, $pageRestrictions[$type] ) ) {
658 $pageRestrictions[$type][] = $level;
664 $sources = array_replace( $tlSources, $ilSources );
666 $cacheEntry[
'cascade_sources'] = [ $sources, $pageRestrictions, $tlSources, $ilSources ];
668 return $cacheEntry[
'cascade_sources'];
682 private function getCascadeProtectionSourcesInternalJoined( PageIdentity $page ): array {
683 if ( !$page->canExist() ) {
684 return [ [], [], [], [] ];
687 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
689 if ( isset( $cacheEntry[
'cascade_sources'] ) ) {
690 return $cacheEntry[
'cascade_sources'];
693 $title = TitleValue::newFromPage( $page );
695 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
696 $baseQuery = $dbr->newSelectQueryBuilder()
705 ->from(
'page_restrictions' )
706 ->join(
'page',
null,
'page_id=pr_page' )
707 ->where( [
'pr_cascade' => 1 ] );
709 $templateQuery = clone $baseQuery;
710 $templateQuery->join(
'templatelinks',
null,
'tl_from=pr_page' )
711 ->fields( [
'type' => $dbr->addQuotes(
'tl' ) ] )
712 ->andWhere( $this->linksMigration->getLinksConditions(
'templatelinks', $title ) );
715 $imageQuery = clone $baseQuery;
716 $imageQuery->join(
'imagelinks',
null,
'il_from=pr_page' )
717 ->fields( [
'type' => $dbr->addQuotes(
'il' ) ] )
718 ->andWhere( $this->linksMigration->getLinksConditions(
'imagelinks', $title ) );
720 $unionQuery = $dbr->newUnionQueryBuilder()
722 ->add( $templateQuery )
724 $res = $unionQuery->caller( __METHOD__ )->fetchResultSet();
726 $res = $templateQuery->caller( __METHOD__ )->fetchResultSet();
731 $pageRestrictions = [];
734 foreach ( $res as $row ) {
735 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
736 if ( $expiry > $now ) {
737 if ( $row->type ===
'il' ) {
738 $ilSources[$row->pr_page] = PageIdentityValue::localIdentity(
740 (
int)$row->page_namespace,
743 } elseif ( $row->type ===
'tl' ) {
744 $tlSources[$row->pr_page] = PageIdentityValue::localIdentity(
746 (
int)$row->page_namespace,
754 if ( !isset( $pageRestrictions[$row->pr_type] ) ) {
755 $pageRestrictions[$row->pr_type] = [];
758 if ( !in_array( $row->pr_level, $pageRestrictions[$row->pr_type] ) ) {
759 $pageRestrictions[$row->pr_type][] = $row->pr_level;
764 $sources = array_replace( $tlSources, $ilSources );
766 $cacheEntry[
'cascade_sources'] = [ $sources, $pageRestrictions, $tlSources, $ilSources ];
768 return $cacheEntry[
'cascade_sources'];
786 private function shouldUseVirtualDomains( PageIdentity $page ): bool {
787 $virtualDomains = $this->options->get( MainConfigNames::VirtualDomainsMapping );
788 return isset( $virtualDomains[TemplateLinksTable::VIRTUAL_DOMAIN] ) ||
789 ( $page->getNamespace() ===
NS_FILE && isset( $virtualDomains[ImageLinksTable::VIRTUAL_DOMAIN] ) );
800 return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'restrictions'] );
812 return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'cascade_sources'] );
824 if ( !$this->areRestrictionsLoaded( $page ) ) {
825 $this->loadRestrictions( $page );
827 return $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'cascade'] ??
false;
840 unset( $this->cache[CacheKeyHelper::getKeyForPage( $page )] );