76 $this->options = $options;
77 $this->wanCache = $wanCache;
78 $this->loadBalancerFactory = $loadBalancerFactory;
79 $this->linkCache = $linkCache;
80 $this->linksMigration = $linksMigration;
81 $this->commentStore = $commentStore;
82 $this->hookContainer = $hookContainer;
83 $this->hookRunner =
new HookRunner( $hookContainer );
84 $this->pageStore = $pageStore;
106 if ( !in_array( $action, $restrictionTypes ) ) {
111 return $restrictions[$action] ?? [];
124 if ( !$this->areRestrictionsLoaded( $page ) ) {
125 $this->loadRestrictions( $page );
127 return $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'restrictions'] ?? [];
141 if ( !$this->areRestrictionsLoaded( $page ) ) {
142 $this->loadRestrictions( $page );
144 return $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'expiry'][$action] ??
null;
160 $protection = $this->getCreateProtectionInternal( $page );
163 if ( $protection[
'permission'] ==
'sysop' ) {
164 $protection[
'permission'] =
'editprotected';
166 if ( $protection[
'permission'] ==
'autoconfirmed' ) {
167 $protection[
'permission'] =
'editsemiprotected';
182 $dbw = $this->loadBalancerFactory->getPrimaryDatabase();
183 $dbw->newDeleteQueryBuilder()
184 ->deleteFrom(
'protected_titles' )
186 ->caller( __METHOD__ )->execute();
187 $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'create_protection'] =
null;
201 $restrictions = $this->getRestrictions( $page, $action );
202 $semi = $this->options->get( MainConfigNames::SemiprotectedRestrictionLevels );
203 if ( !$restrictions || !$semi ) {
209 foreach ( array_keys( $semi,
'editsemiprotected' ) as $key ) {
210 $semi[$key] =
'autoconfirmed';
212 foreach ( array_keys( $restrictions,
'editsemiprotected' ) as $key ) {
213 $restrictions[$key] =
'autoconfirmed';
216 return !array_diff( $restrictions, $semi );
235 $applicableTypes = $this->listApplicableRestrictionTypes( $page );
237 if ( $action ===
'' ) {
238 foreach ( $applicableTypes as $type ) {
239 if ( $this->isProtected( $page, $type ) ) {
246 if ( !in_array( $action, $applicableTypes ) ) {
250 return (
bool)array_diff(
252 $this->getRestrictions( $page, $action ),
253 $this->options->get( MainConfigNames::RestrictionLevels )
268 return $this->shouldUseVirtualDomains()
269 ? $this->getCascadeProtectionSourcesInternal( $page )[0] !== []
270 : $this->getCascadeProtectionSourcesInternalJoined( $page )[0] !== [];
286 $types = $this->listAllRestrictionTypes( $page->
exists() );
290 $types = array_values( array_diff( $types, [
'upload' ] ) );
293 if ( $this->hookContainer->isRegistered(
'TitleGetRestrictionTypes' ) ) {
294 $this->hookRunner->onTitleGetRestrictionTypes(
295 Title::newFromPageIdentity( $page ), $types );
312 return array_values( array_diff( $types, [
'create' ] ) );
316 return array_values( array_intersect( $types, [
'create' ] ) );
328 PageIdentity $page,
int $flags = IDBAccessObject::READ_NORMAL
336 $readLatest = DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST );
338 if ( $this->areRestrictionsLoaded( $page ) && !$readLatest ) {
342 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
344 $cacheEntry[
'restrictions'] = [];
348 $page = $this->pageStore->getPageByReference( $page, $flags ) ?? $page;
350 $id = $page->
getId();
353 $loadRestrictionsFromDb =
static function ( IReadableDatabase $dbr ) use ( $fname, $id ) {
354 return iterator_to_array(
355 $dbr->newSelectQueryBuilder()
356 ->select( [
'pr_type',
'pr_expiry',
'pr_level',
'pr_cascade' ] )
357 ->from(
'page_restrictions' )
358 ->where( [
'pr_page' => $id ] )
359 ->caller( $fname )->fetchResultSet()
364 $dbr = $this->loadBalancerFactory->getPrimaryDatabase();
365 $rows = $loadRestrictionsFromDb( $dbr );
367 $this->pageStore->getPageForLink( TitleValue::newFromPage( $page ) )->getId();
368 $latestRev = $this->linkCache->getGoodLinkFieldObj( $page,
'revision' );
375 $rows = $this->wanCache->getWithSetCallback(
377 $this->wanCache->makeKey(
'page-restrictions',
'v1', $id, $latestRev ),
378 $this->wanCache::TTL_DAY,
379 function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) {
380 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
381 $setOpts += Database::getCacheSetOptions( $dbr );
382 if ( $this->loadBalancerFactory->hasOrMadeRecentPrimaryChanges() ) {
384 $ttl = WANObjectCache::TTL_UNCACHEABLE;
387 return $loadRestrictionsFromDb( $dbr );
393 $this->loadRestrictionsFromRows( $page, $rows );
395 $titleProtection = $this->getCreateProtectionInternal( $page );
397 if ( $titleProtection ) {
399 $expiry = $titleProtection[
'expiry'];
401 if ( !$expiry || $expiry > $now ) {
403 $cacheEntry[
'expiry'][
'create'] = $expiry ?:
null;
404 $cacheEntry[
'restrictions'][
'create'] =
405 explode(
',', trim( $titleProtection[
'permission'] ) );
408 $cacheEntry[
'create_protection'] =
null;
411 $cacheEntry[
'expiry'][
'create'] =
'infinity';
428 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
430 $restrictionTypes = $this->listApplicableRestrictionTypes( $page );
432 foreach ( $restrictionTypes as $type ) {
433 $cacheEntry[
'restrictions'][$type] = [];
434 $cacheEntry[
'expiry'][$type] =
'infinity';
437 $cacheEntry[
'cascade'] =
false;
447 foreach ( $rows as $row ) {
449 if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
453 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
454 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
459 if ( !$expiry || $expiry > $now ) {
460 $cacheEntry[
'expiry'][$row->pr_type] = $expiry ?:
null;
461 $cacheEntry[
'restrictions'][$row->pr_type]
462 = explode(
',', trim( $row->pr_level ) );
463 if ( $row->pr_cascade ) {
464 $cacheEntry[
'cascade'] =
true;
480 private function getCreateProtectionInternal( PageIdentity $page ): ?array {
482 if ( !$page->canExist() ) {
491 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
493 if ( !$cacheEntry || !array_key_exists(
'create_protection', $cacheEntry ) ) {
494 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
495 $commentQuery = $this->commentStore->getJoin(
'pt_reason' );
496 $row = $dbr->newSelectQueryBuilder()
497 ->select( [
'pt_user',
'pt_expiry',
'pt_create_perm' ] )
498 ->from(
'protected_titles' )
500 ->queryInfo( $commentQuery )
501 ->caller( __METHOD__ )
505 $cacheEntry[
'create_protection'] = [
506 'user' => $row->pt_user,
507 'expiry' => $dbr->decodeExpiry( $row->pt_expiry ),
508 'permission' => $row->pt_create_perm,
509 'reason' => $this->commentStore->getComment(
'pt_reason', $row )->text,
512 $cacheEntry[
'create_protection'] =
null;
517 return $cacheEntry[
'create_protection'];
535 return $this->shouldUseVirtualDomains()
536 ? $this->getCascadeProtectionSourcesInternal( $page )
537 : $this->getCascadeProtectionSourcesInternalJoined( $page );
551 private function getCascadeProtectionSourcesInternal(
554 if ( !$page->canExist() ) {
555 return [ [], [], [], [] ];
558 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
560 if ( isset( $cacheEntry[
'cascade_sources'] ) ) {
561 return $cacheEntry[
'cascade_sources'];
564 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
567 $cascadeRestrictions = $dbr->newSelectQueryBuilder()
576 ->from(
'page_restrictions' )
577 ->join(
'page',
null,
'page_id=pr_page' )
578 ->where( [
'pr_cascade' => 1 ] )
579 ->caller( __METHOD__ )
582 if ( $cascadeRestrictions->numRows() === 0 ) {
583 return [ [], [], [], [] ];
586 $restrictionsByPage = [];
587 foreach ( $cascadeRestrictions as $row ) {
588 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
589 if ( $expiry > $now ) {
590 if ( !isset( $restrictionsByPage[$row->pr_page] ) ) {
591 $restrictionsByPage[$row->pr_page] = [
592 'title' => PageIdentityValue::localIdentity(
594 (
int)$row->page_namespace,
598 $row->pr_type => $row->pr_level
602 $restrictionsByPage[$row->pr_page][
'restrictions'][$row->pr_type] = $row->pr_level;
607 if ( $restrictionsByPage === [] ) {
608 return [ [], [], [], [] ];
611 $title = TitleValue::newFromPage( $page );
613 $templateLinksDb = $this->loadBalancerFactory->getReplicaDatabase( TemplateLinksTable::VIRTUAL_DOMAIN );
614 $templateLinks = $templateLinksDb->newSelectQueryBuilder()
615 ->select(
'tl_from' )
616 ->from(
'templatelinks' )
617 ->where( [
'tl_from' => array_keys( $restrictionsByPage ) ] )
618 ->andWhere( $this->linksMigration->getLinksConditions(
'templatelinks', $title ) )
619 ->caller( __METHOD__ )
624 $pageRestrictions = [];
626 foreach ( $templateLinks as $link ) {
627 $pageData = $restrictionsByPage[$link->tl_from];
628 $tlSources[$link->tl_from] = $pageData[
'title'];
629 foreach ( $pageData[
'restrictions'] as $type => $level ) {
630 if ( !isset( $pageRestrictions[$type] ) ) {
631 $pageRestrictions[$type] = [];
634 if ( !in_array( $level, $pageRestrictions[$type] ) ) {
635 $pageRestrictions[$type][] = $level;
641 $imageLinksDb = $this->loadBalancerFactory->getReplicaDatabase( ImageLinksTable::VIRTUAL_DOMAIN );
642 $imageLinks = $imageLinksDb->newSelectQueryBuilder()
643 ->select(
'il_from' )
644 ->from(
'imagelinks' )
645 ->where( [
'il_from' => array_keys( $restrictionsByPage ) ] )
646 ->andWhere( $this->linksMigration->getLinksConditions(
'imagelinks', $title ) )
647 ->caller( __METHOD__ )
650 foreach ( $imageLinks as $link ) {
651 $pageData = $restrictionsByPage[$link->il_from];
652 $ilSources[$link->il_from] = $pageData[
'title'];
653 foreach ( $pageData[
'restrictions'] as $type => $level ) {
654 if ( !isset( $pageRestrictions[$type] ) ) {
655 $pageRestrictions[$type] = [];
658 if ( !in_array( $level, $pageRestrictions[$type] ) ) {
659 $pageRestrictions[$type][] = $level;
665 $sources = array_replace( $tlSources, $ilSources );
667 $cacheEntry[
'cascade_sources'] = [ $sources, $pageRestrictions, $tlSources, $ilSources ];
669 return $cacheEntry[
'cascade_sources'];
683 private function getCascadeProtectionSourcesInternalJoined( PageIdentity $page ): array {
684 if ( !$page->canExist() ) {
685 return [ [], [], [], [] ];
688 $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )];
690 if ( isset( $cacheEntry[
'cascade_sources'] ) ) {
691 return $cacheEntry[
'cascade_sources'];
694 $title = TitleValue::newFromPage( $page );
696 $dbr = $this->loadBalancerFactory->getReplicaDatabase();
697 $baseQuery = $dbr->newSelectQueryBuilder()
706 ->from(
'page_restrictions' )
707 ->join(
'page',
null,
'page_id=pr_page' )
708 ->where( [
'pr_cascade' => 1 ] );
710 $templateQuery = clone $baseQuery;
711 $templateQuery->join(
'templatelinks',
null,
'tl_from=pr_page' )
712 ->fields( [
'type' => $dbr->addQuotes(
'tl' ) ] )
713 ->andWhere( $this->linksMigration->getLinksConditions(
'templatelinks', $title ) );
716 $imageQuery = clone $baseQuery;
717 $imageQuery->join(
'imagelinks',
null,
'il_from=pr_page' )
718 ->fields( [
'type' => $dbr->addQuotes(
'il' ) ] )
719 ->andWhere( $this->linksMigration->getLinksConditions(
'imagelinks', $title ) );
721 $unionQuery = $dbr->newUnionQueryBuilder()
723 ->add( $templateQuery )
725 $res = $unionQuery->caller( __METHOD__ )->fetchResultSet();
727 $res = $templateQuery->caller( __METHOD__ )->fetchResultSet();
732 $pageRestrictions = [];
735 foreach ( $res as $row ) {
736 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
737 if ( $expiry > $now ) {
738 if ( $row->type ===
'il' ) {
739 $ilSources[$row->pr_page] = PageIdentityValue::localIdentity(
741 (
int)$row->page_namespace,
744 } elseif ( $row->type ===
'tl' ) {
745 $tlSources[$row->pr_page] = PageIdentityValue::localIdentity(
747 (
int)$row->page_namespace,
755 if ( !isset( $pageRestrictions[$row->pr_type] ) ) {
756 $pageRestrictions[$row->pr_type] = [];
759 if ( !in_array( $row->pr_level, $pageRestrictions[$row->pr_type] ) ) {
760 $pageRestrictions[$row->pr_type][] = $row->pr_level;
765 $sources = array_replace( $tlSources, $ilSources );
767 $cacheEntry[
'cascade_sources'] = [ $sources, $pageRestrictions, $tlSources, $ilSources ];
769 return $cacheEntry[
'cascade_sources'];
786 private function shouldUseVirtualDomains(): bool {
787 $virtualDomains = $this->options->get( MainConfigNames::VirtualDomainsMapping );
788 return isset( $virtualDomains[LinksTable::VIRTUAL_DOMAIN] );
799 return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'restrictions'] );
811 return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'cascade_sources'] );
823 if ( !$this->areRestrictionsLoaded( $page ) ) {
824 $this->loadRestrictions( $page );
826 return $this->cache[CacheKeyHelper::getKeyForPage( $page )][
'cascade'] ??
false;
839 unset( $this->cache[CacheKeyHelper::getKeyForPage( $page )] );