MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
26 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
50 use Wikimedia\Assert\Assert;
51 use Wikimedia\Assert\PreconditionException;
52 use Wikimedia\NonSerializable\NonSerializableTrait;
56 
64  use NonSerializableTrait;
65  use ProtectedHookAccessorTrait;
67 
68  // Constants for $mDataLoadedFrom and related
69 
75  public $mTitle;
76 
82  public $mDataLoaded = false;
83 
88  private $mPageIsRedirectField = false;
89 
96  private $mHasRedirectTarget = null;
97 
103  protected $mRedirectTarget = null;
104 
108  private $mIsNew = false;
109 
113  private $mIsRedirect = false;
114 
120  public $mLatest = false;
121 
127  public $mPreparedEdit = false;
128 
132  protected $mId = null;
133 
138 
142  private $mLastRevision = null;
143 
147  protected $mTimestamp = '';
148 
152  protected $mTouched = '19700101000000';
153 
157  protected $mLanguage = null;
158 
162  protected $mLinksUpdated = '19700101000000';
163 
167  private $derivedDataUpdater = null;
168 
172  public function __construct( PageIdentity $pageIdentity ) {
173  $pageIdentity->assertWiki( PageIdentity::LOCAL );
174 
175  // TODO: remove the need for casting to Title.
176  $title = Title::castFromPageIdentity( $pageIdentity );
177  if ( !$title->canExist() ) {
178  throw new InvalidArgumentException( "WikiPage constructed on a Title that cannot exist as a page: $title" );
179  }
180 
181  $this->mTitle = $title;
182  }
183 
188  public function __clone() {
189  $this->mTitle = clone $this->mTitle;
190  }
191 
203  public static function factory( PageIdentity $pageIdentity ) {
204  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $pageIdentity );
205  }
206 
218  public static function newFromID( $id, $from = 'fromdb' ) {
219  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromID( $id, $from );
220  }
221 
234  public static function newFromRow( $row, $from = 'fromdb' ) {
235  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromRow( $row, $from );
236  }
237 
244  public static function convertSelectType( $type ) {
245  switch ( $type ) {
246  case 'fromdb':
247  return self::READ_NORMAL;
248  case 'fromdbmaster':
249  return self::READ_LATEST;
250  case 'forupdate':
251  return self::READ_LOCKING;
252  default:
253  // It may already be an integer or whatever else
254  return $type;
255  }
256  }
257 
262  return MediaWikiServices::getInstance()->getPageUpdaterFactory();
263  }
264 
268  private function getRevisionStore() {
269  return MediaWikiServices::getInstance()->getRevisionStore();
270  }
271 
276  return MediaWikiServices::getInstance()->getContentHandlerFactory();
277  }
278 
282  private function getDBLoadBalancer() {
283  return MediaWikiServices::getInstance()->getDBLoadBalancer();
284  }
285 
292  public function getActionOverrides() {
293  return $this->getContentHandler()->getActionOverrides();
294  }
295 
305  public function getContentHandler() {
306  return $this->getContentHandlerFactory()
307  ->getContentHandler( $this->getContentModel() );
308  }
309 
314  public function getTitle(): Title {
315  return $this->mTitle;
316  }
317 
322  public function clear() {
323  $this->mDataLoaded = false;
324  $this->mDataLoadedFrom = self::READ_NONE;
325 
326  $this->clearCacheFields();
327  }
328 
333  protected function clearCacheFields() {
334  $this->mId = null;
335  $this->mRedirectTarget = null; // Title object if set
336  $this->mHasRedirectTarget = null;
337  $this->mPageIsRedirectField = false;
338  $this->mLastRevision = null; // Latest revision
339  $this->mTouched = '19700101000000';
340  $this->mLanguage = null;
341  $this->mLinksUpdated = '19700101000000';
342  $this->mTimestamp = '';
343  $this->mIsNew = false;
344  $this->mIsRedirect = false;
345  $this->mLatest = false;
346  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
347  // checks the requested rev ID and content against the cached one. For most
348  // content types, the output should not change during the lifetime of this cache.
349  // Clearing it can cause extra parses on edit for no reason.
350  }
351 
357  public function clearPreparedEdit() {
358  $this->mPreparedEdit = false;
359  }
360 
370  public static function getQueryInfo() {
371  $pageLanguageUseDB = MediaWikiServices::getInstance()->getMainConfig()->get( 'PageLanguageUseDB' );
372 
373  $ret = [
374  'tables' => [ 'page' ],
375  'fields' => [
376  'page_id',
377  'page_namespace',
378  'page_title',
379  'page_restrictions',
380  'page_is_redirect',
381  'page_is_new',
382  'page_random',
383  'page_touched',
384  'page_links_updated',
385  'page_latest',
386  'page_len',
387  'page_content_model',
388  ],
389  'joins' => [],
390  ];
391 
392  if ( $pageLanguageUseDB ) {
393  $ret['fields'][] = 'page_lang';
394  }
395 
396  return $ret;
397  }
398 
406  protected function pageData( $dbr, $conditions, $options = [] ) {
407  $pageQuery = self::getQueryInfo();
408 
409  $this->getHookRunner()->onArticlePageDataBefore(
410  $this, $pageQuery['fields'], $pageQuery['tables'], $pageQuery['joins'] );
411 
412  $row = $dbr->selectRow(
413  $pageQuery['tables'],
414  $pageQuery['fields'],
415  $conditions,
416  __METHOD__,
417  $options,
418  $pageQuery['joins']
419  );
420 
421  $this->getHookRunner()->onArticlePageDataAfter( $this, $row );
422 
423  return $row;
424  }
425 
435  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
436  if ( !$title->canExist() ) {
437  return false;
438  }
439 
440  return $this->pageData( $dbr, [
441  'page_namespace' => $title->getNamespace(),
442  'page_title' => $title->getDBkey() ], $options );
443  }
444 
453  public function pageDataFromId( $dbr, $id, $options = [] ) {
454  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
455  }
456 
469  public function loadPageData( $from = 'fromdb' ) {
470  $from = self::convertSelectType( $from );
471  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
472  // We already have the data from the correct location, no need to load it twice.
473  return;
474  }
475 
476  if ( is_int( $from ) ) {
477  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
478  $loadBalancer = $this->getDBLoadBalancer();
479  $db = $loadBalancer->getConnection( $index );
480  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
481 
482  if ( !$data
483  && $index == DB_REPLICA
484  && $loadBalancer->getServerCount() > 1
485  && $loadBalancer->hasOrMadeRecentPrimaryChanges()
486  ) {
487  $from = self::READ_LATEST;
488  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
489  $db = $loadBalancer->getConnection( $index );
490  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
491  }
492  } else {
493  // No idea from where the caller got this data, assume replica DB.
494  $data = $from;
495  $from = self::READ_NORMAL;
496  }
497 
498  $this->loadFromRow( $data, $from );
499  }
500 
514  public function wasLoadedFrom( $from ) {
515  $from = self::convertSelectType( $from );
516 
517  if ( !is_int( $from ) ) {
518  // No idea from where the caller got this data, assume replica DB.
519  $from = self::READ_NORMAL;
520  }
521 
522  if ( $from <= $this->mDataLoadedFrom ) {
523  return true;
524  }
525 
526  return false;
527  }
528 
540  public function loadFromRow( $data, $from ) {
541  $lc = MediaWikiServices::getInstance()->getLinkCache();
542  $lc->clearLink( $this->mTitle );
543 
544  if ( $data ) {
545  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
546 
547  $this->mTitle->loadFromRow( $data );
548 
549  // Old-fashioned restrictions
550  $this->mTitle->loadRestrictions( $data->page_restrictions );
551 
552  $this->mId = intval( $data->page_id );
553  $this->mTouched = MWTimestamp::convert( TS_MW, $data->page_touched );
554  $this->mLanguage = $data->page_lang ?? null;
555  $this->mLinksUpdated = $data->page_links_updated === null
556  ? null
557  : MWTimestamp::convert( TS_MW, $data->page_links_updated );
558  $this->mPageIsRedirectField = (bool)$data->page_is_redirect;
559  $this->mIsNew = intval( $data->page_is_new ?? 0 );
560  $this->mIsRedirect = intval( $data->page_is_redirect ?? 0 );
561  $this->mLatest = intval( $data->page_latest );
562  // T39225: $latest may no longer match the cached latest RevisionRecord object.
563  // Double-check the ID of any cached latest RevisionRecord object for consistency.
564  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
565  $this->mLastRevision = null;
566  $this->mTimestamp = '';
567  }
568  } else {
569  $lc->addBadLinkObj( $this->mTitle );
570 
571  $this->mTitle->loadFromRow( false );
572 
573  $this->clearCacheFields();
574 
575  $this->mId = 0;
576  }
577 
578  $this->mDataLoaded = true;
579  $this->mDataLoadedFrom = self::convertSelectType( $from );
580  }
581 
587  public function getId( $wikiId = self::LOCAL ): int {
588  $this->assertWiki( $wikiId );
589 
590  if ( !$this->mDataLoaded ) {
591  $this->loadPageData();
592  }
593  return $this->mId;
594  }
595 
599  public function exists(): bool {
600  if ( !$this->mDataLoaded ) {
601  $this->loadPageData();
602  }
603  return $this->mId > 0;
604  }
605 
614  public function hasViewableContent() {
615  return $this->mTitle->isKnown();
616  }
617 
624  public function isRedirect() {
625  if ( !$this->mDataLoaded ) {
626  $this->loadPageData();
627  }
628 
629  return (bool)$this->mIsRedirect;
630  }
631 
641  public function getPageIsRedirectField() {
642  if ( !$this->mDataLoaded ) {
643  $this->loadPageData();
644  }
646  }
647 
656  public function isNew() {
657  if ( !$this->mDataLoaded ) {
658  $this->loadPageData();
659  }
660 
661  return (bool)$this->mIsNew;
662  }
663 
674  public function getContentModel() {
675  if ( $this->exists() ) {
676  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
677 
678  return $cache->getWithSetCallback(
679  $cache->makeKey( 'page-content-model', $this->getLatest() ),
680  $cache::TTL_MONTH,
681  function () {
682  $rev = $this->getRevisionRecord();
683  if ( $rev ) {
684  // Look at the revision's actual content model
685  $slot = $rev->getSlot(
686  SlotRecord::MAIN,
687  RevisionRecord::RAW
688  );
689  return $slot->getModel();
690  } else {
691  LoggerFactory::getInstance( 'wikipage' )->warning(
692  'Page exists but has no (visible) revisions!',
693  [
694  'page-title' => $this->mTitle->getPrefixedDBkey(),
695  'page-id' => $this->getId(),
696  ]
697  );
698  return $this->mTitle->getContentModel();
699  }
700  },
701  [ 'pcTTL' => $cache::TTL_PROC_LONG ]
702  );
703  }
704 
705  // use the default model for this page
706  return $this->mTitle->getContentModel();
707  }
708 
713  public function checkTouched() {
714  return ( $this->exists() && !$this->isRedirect() );
715  }
716 
721  public function getTouched() {
722  if ( !$this->mDataLoaded ) {
723  $this->loadPageData();
724  }
725  return $this->mTouched;
726  }
727 
731  public function getLanguage() {
732  if ( !$this->mDataLoaded ) {
733  $this->loadLastEdit();
734  }
735 
736  return $this->mLanguage;
737  }
738 
743  public function getLinksTimestamp() {
744  if ( !$this->mDataLoaded ) {
745  $this->loadPageData();
746  }
747  return $this->mLinksUpdated;
748  }
749 
755  public function getLatest( $wikiId = self::LOCAL ) {
756  $this->assertWiki( $wikiId );
757 
758  if ( !$this->mDataLoaded ) {
759  $this->loadPageData();
760  }
761  return (int)$this->mLatest;
762  }
763 
768  protected function loadLastEdit() {
769  if ( $this->mLastRevision !== null ) {
770  return; // already loaded
771  }
772 
773  $latest = $this->getLatest();
774  if ( !$latest ) {
775  return; // page doesn't exist or is missing page_latest info
776  }
777 
778  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
779  // T39225: if session S1 loads the page row FOR UPDATE, the result always
780  // includes the latest changes committed. This is true even within REPEATABLE-READ
781  // transactions, where S1 normally only sees changes committed before the first S1
782  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
783  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
784  // happened after the first S1 SELECT.
785  // https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html#isolevel_repeatable-read
786  $revision = $this->getRevisionStore()
787  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LOCKING );
788  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
789  // Bug T93976: if page_latest was loaded from the primary DB, fetch the
790  // revision from there as well, as it may not exist yet on a replica DB.
791  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
792  $revision = $this->getRevisionStore()
793  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LATEST );
794  } else {
795  $revision = $this->getRevisionStore()->getKnownCurrentRevision( $this->getTitle(), $latest );
796  }
797 
798  if ( $revision ) {
799  $this->setLastEdit( $revision );
800  }
801  }
802 
807  private function setLastEdit( RevisionRecord $revRecord ) {
808  $this->mLastRevision = $revRecord;
809  $this->mLatest = $revRecord->getId();
810  $this->mTimestamp = $revRecord->getTimestamp();
811  $this->mTouched = max( $this->mTouched, $revRecord->getTimestamp() );
812  }
813 
819  public function getRevisionRecord() {
820  $this->loadLastEdit();
821  if ( $this->mLastRevision ) {
822  return $this->mLastRevision;
823  }
824  return null;
825  }
826 
840  public function getContent( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
841  $this->loadLastEdit();
842  if ( $this->mLastRevision ) {
843  return $this->mLastRevision->getContent( SlotRecord::MAIN, $audience, $performer );
844  }
845  return null;
846  }
847 
851  public function getTimestamp() {
852  // Check if the field has been filled by WikiPage::setTimestamp()
853  if ( !$this->mTimestamp ) {
854  $this->loadLastEdit();
855  }
856 
857  return MWTimestamp::convert( TS_MW, $this->mTimestamp );
858  }
859 
865  public function setTimestamp( $ts ) {
866  $this->mTimestamp = MWTimestamp::convert( TS_MW, $ts );
867  }
868 
879  public function getUser( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
880  $this->loadLastEdit();
881  if ( $this->mLastRevision ) {
882  $revUser = $this->mLastRevision->getUser( $audience, $performer );
883  return $revUser ? $revUser->getId() : 0;
884  } else {
885  return -1;
886  }
887  }
888 
900  public function getCreator( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
901  $revRecord = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
902  if ( $revRecord ) {
903  return $revRecord->getUser( $audience, $performer );
904  } else {
905  return null;
906  }
907  }
908 
919  public function getUserText( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
920  $this->loadLastEdit();
921  if ( $this->mLastRevision ) {
922  $revUser = $this->mLastRevision->getUser( $audience, $performer );
923  return $revUser ? $revUser->getName() : '';
924  } else {
925  return '';
926  }
927  }
928 
940  public function getComment( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
941  $this->loadLastEdit();
942  if ( $this->mLastRevision ) {
943  $revComment = $this->mLastRevision->getComment( $audience, $performer );
944  return $revComment ? $revComment->text : '';
945  } else {
946  return '';
947  }
948  }
949 
955  public function getMinorEdit() {
956  $this->loadLastEdit();
957  if ( $this->mLastRevision ) {
958  return $this->mLastRevision->isMinor();
959  } else {
960  return false;
961  }
962  }
963 
974  public function isCountable( $editInfo = false ) {
975  $articleCountMethod = MediaWikiServices::getInstance()->getMainConfig()->get( 'ArticleCountMethod' );
976 
977  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
978 
979  if ( !$this->mTitle->isContentPage() ) {
980  return false;
981  }
982 
983  if ( $editInfo instanceof PreparedEdit ) {
984  // NOTE: only the main slot can make a page a redirect
985  $content = $editInfo->pstContent;
986  } elseif ( $editInfo instanceof PreparedUpdate ) {
987  // NOTE: only the main slot can make a page a redirect
988  $content = $editInfo->getRawContent( SlotRecord::MAIN );
989  } else {
990  $content = $this->getContent();
991  }
992 
993  if ( !$content || $content->isRedirect() ) {
994  return false;
995  }
996 
997  $hasLinks = null;
998 
999  if ( $articleCountMethod === 'link' ) {
1000  // nasty special case to avoid re-parsing to detect links
1001 
1002  if ( $editInfo ) {
1003  // ParserOutput::getLinks() is a 2D array of page links, so
1004  // to be really correct we would need to recurse in the array
1005  // but the main array should only have items in it if there are
1006  // links.
1007  $hasLinks = (bool)count( $editInfo->output->getLinks() );
1008  } else {
1009  // NOTE: keep in sync with RevisionRenderer::getLinkCount
1010  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
1011  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', '1',
1012  [ 'pl_from' => $this->getId() ], __METHOD__ );
1013  }
1014  }
1015 
1016  // TODO: MCR: determine $hasLinks for each slot, and use that info
1017  // with that slot's Content's isCountable method. That requires per-
1018  // slot ParserOutput in the ParserCache, or per-slot info in the
1019  // pagelinks table.
1020  return $content->isCountable( $hasLinks );
1021  }
1022 
1033  public function getRedirectTarget() {
1034  if ( $this->mRedirectTarget !== null ) {
1035  return $this->mRedirectTarget;
1036  }
1037 
1038  if ( $this->mHasRedirectTarget === false || !$this->getPageIsRedirectField() ) {
1039  return null;
1040  }
1041 
1042  // Query the redirect table
1043  $dbr = wfGetDB( DB_REPLICA );
1044  $row = $dbr->selectRow( 'redirect',
1045  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1046  [ 'rd_from' => $this->getId() ],
1047  __METHOD__
1048  );
1049 
1050  // rd_fragment and rd_interwiki were added later, populate them if empty
1051  if ( $row && $row->rd_fragment !== null && $row->rd_interwiki !== null ) {
1052  // (T203942) We can't redirect to Media namespace because it's virtual.
1053  // We don't want to modify Title objects farther down the
1054  // line. So, let's fix this here by changing to File namespace.
1055  if ( $row->rd_namespace == NS_MEDIA ) {
1056  $namespace = NS_FILE;
1057  } else {
1058  $namespace = $row->rd_namespace;
1059  }
1060  // T261347: be defensive when fetching data from the redirect table.
1061  // Use Title::makeTitleSafe(), and if that returns null, ignore the
1062  // row. In an ideal world, the DB would be cleaned up after a
1063  // namespace change, but nobody could be bothered to do that.
1064  $this->mRedirectTarget = Title::makeTitleSafe(
1065  $namespace, $row->rd_title,
1066  $row->rd_fragment, $row->rd_interwiki
1067  );
1068  $this->mHasRedirectTarget = $this->mRedirectTarget !== null;
1069  return $this->mRedirectTarget;
1070  }
1071 
1072  // This page doesn't have an entry in the redirect table
1073  $this->mRedirectTarget = $this->insertRedirect();
1074  $this->mHasRedirectTarget = $this->mRedirectTarget !== null;
1075  return $this->mRedirectTarget;
1076  }
1077 
1086  public function insertRedirect() {
1087  $content = $this->getContent();
1088  $retval = $content ? $content->getRedirectTarget() : null;
1089  if ( !$retval ) {
1090  return null;
1091  }
1092 
1093  // Update the DB post-send if the page has not cached since now
1094  $latest = $this->getLatest();
1096  function () use ( $retval, $latest ) {
1097  $this->insertRedirectEntry( $retval, $latest );
1098  },
1099  DeferredUpdates::POSTSEND,
1100  wfGetDB( DB_PRIMARY )
1101  );
1102 
1103  return $retval;
1104  }
1105 
1112  public function insertRedirectEntry( LinkTarget $rt, $oldLatest = null ) {
1113  $rt = Title::castFromLinkTarget( $rt );
1114  if ( !$rt->isValidRedirectTarget() ) {
1115  // Don't put a bad redirect into the database (T278367)
1116  return false;
1117  }
1118 
1119  $dbw = wfGetDB( DB_PRIMARY );
1120  $dbw->startAtomic( __METHOD__ );
1121 
1122  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1123  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1124  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1125  $dbw->upsert(
1126  'redirect',
1127  [
1128  'rd_from' => $this->getId(),
1129  'rd_namespace' => $rt->getNamespace(),
1130  'rd_title' => $rt->getDBkey(),
1131  'rd_fragment' => $truncatedFragment,
1132  'rd_interwiki' => $rt->getInterwiki(),
1133  ],
1134  'rd_from',
1135  [
1136  'rd_namespace' => $rt->getNamespace(),
1137  'rd_title' => $rt->getDBkey(),
1138  'rd_fragment' => $truncatedFragment,
1139  'rd_interwiki' => $rt->getInterwiki(),
1140  ],
1141  __METHOD__
1142  );
1143  $success = true;
1144  } else {
1145  $success = false;
1146  }
1147 
1148  $dbw->endAtomic( __METHOD__ );
1149 
1150  return $success;
1151  }
1152 
1158  public function followRedirect() {
1159  return $this->getRedirectURL( $this->getRedirectTarget() );
1160  }
1161 
1169  public function getRedirectURL( $rt ) {
1170  if ( !$rt ) {
1171  return false;
1172  }
1173 
1174  if ( $rt->isExternal() ) {
1175  if ( $rt->isLocal() ) {
1176  // Offsite wikis need an HTTP redirect.
1177  // This can be hard to reverse and may produce loops,
1178  // so they may be disabled in the site configuration.
1179  $source = $this->mTitle->getFullURL( 'redirect=no' );
1180  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1181  } else {
1182  // External pages without "local" bit set are not valid
1183  // redirect targets
1184  return false;
1185  }
1186  }
1187 
1188  if ( $rt->isSpecialPage() ) {
1189  // Gotta handle redirects to special pages differently:
1190  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1191  // Some pages are not valid targets.
1192  if ( $rt->isValidRedirectTarget() ) {
1193  return $rt->getFullURL();
1194  } else {
1195  return false;
1196  }
1197  } elseif ( !$rt->isValidRedirectTarget() ) {
1198  // We somehow got a bad redirect target into the database (T278367)
1199  return false;
1200  }
1201 
1202  return $rt;
1203  }
1204 
1210  public function getContributors() {
1211  // @todo: This is expensive; cache this info somewhere.
1212 
1213  $dbr = wfGetDB( DB_REPLICA );
1214 
1215  $actorMigration = ActorMigration::newMigration();
1216  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1217 
1218  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1219 
1220  $revactor_actor = $actorQuery['fields']['rev_actor'];
1221  $fields = [
1222  'user_id' => $actorQuery['fields']['rev_user'],
1223  'user_name' => $actorQuery['fields']['rev_user_text'],
1224  'actor_id' => "MIN($revactor_actor)",
1225  'user_real_name' => 'MIN(user_real_name)',
1226  'timestamp' => 'MAX(rev_timestamp)',
1227  ];
1228 
1229  $conds = [ 'rev_page' => $this->getId() ];
1230 
1231  // The user who made the top revision gets credited as "this page was last edited by
1232  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1233  $user = $this->getUser()
1234  ? User::newFromId( $this->getUser() )
1235  : User::newFromName( $this->getUserText(), false );
1236  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1237 
1238  // Username hidden?
1239  $conds[] = "{$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER )} = 0";
1240 
1241  $jconds = [
1242  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1243  ] + $actorQuery['joins'];
1244 
1245  $options = [
1246  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1247  'ORDER BY' => 'timestamp DESC',
1248  ];
1249 
1250  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1251  return new UserArrayFromResult( $res );
1252  }
1253 
1261  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1262  // NOTE: Keep in sync with ParserOutputAccess::shouldUseCache().
1263  // TODO: Once ParserOutputAccess is stable, deprecated this method.
1264  return $this->exists()
1265  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1266  && $this->getContentHandler()->isParserCacheSupported();
1267  }
1268 
1284  public function getParserOutput(
1285  ?ParserOptions $parserOptions = null, $oldid = null, $noCache = false
1286  ) {
1287  if ( $oldid ) {
1288  $revision = $this->getRevisionStore()->getRevisionByTitle( $this->getTitle(), $oldid );
1289 
1290  if ( !$revision ) {
1291  return false;
1292  }
1293  } else {
1294  $revision = $this->getRevisionRecord();
1295  }
1296 
1297  if ( !$parserOptions ) {
1298  $parserOptions = ParserOptions::newCanonical( 'canonical' );
1299  }
1300 
1301  $options = $noCache ? ParserOutputAccess::OPT_NO_CACHE : 0;
1302 
1303  $status = MediaWikiServices::getInstance()->getParserOutputAccess()->getParserOutput(
1304  $this, $parserOptions, $revision, $options
1305  );
1306  return $status->isOK() ? $status->getValue() : false; // convert null to false
1307  }
1308 
1314  public function doViewUpdates( Authority $performer, $oldid = 0 ) {
1315  if ( wfReadOnly() ) {
1316  return;
1317  }
1318 
1319  // Update newtalk / watchlist notification status;
1320  // Avoid outage if the primary DB is not reachable by using a deferred updated
1322  function () use ( $performer, $oldid ) {
1323  $legacyUser = MediaWikiServices::getInstance()
1324  ->getUserFactory()
1325  ->newFromAuthority( $performer );
1326  $this->getHookRunner()->onPageViewUpdates( $this, $legacyUser );
1327 
1328  MediaWikiServices::getInstance()
1329  ->getWatchlistManager()
1330  ->clearTitleUserNotifications( $performer, $this, $oldid );
1331  },
1332  DeferredUpdates::PRESEND
1333  );
1334  }
1335 
1342  public function doPurge() {
1343  if ( !$this->getHookRunner()->onArticlePurge( $this ) ) {
1344  return false;
1345  }
1346 
1347  $this->mTitle->invalidateCache();
1348 
1349  // Clear file cache and send purge after above page_touched update was committed
1350  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1351  $hcu->purgeTitleUrls( $this->mTitle, $hcu::PURGE_PRESEND );
1352 
1353  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) {
1354  MediaWikiServices::getInstance()->getMessageCache()
1355  ->updateMessageOverride( $this->mTitle, $this->getContent() );
1356  }
1357 
1358  return true;
1359  }
1360 
1377  public function insertOn( $dbw, $pageId = null ) {
1378  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1379  $dbw->insert(
1380  'page',
1381  [
1382  'page_namespace' => $this->mTitle->getNamespace(),
1383  'page_title' => $this->mTitle->getDBkey(),
1384  'page_restrictions' => '',
1385  'page_is_redirect' => 0, // Will set this shortly...
1386  'page_is_new' => 1,
1387  'page_random' => wfRandom(),
1388  'page_touched' => $dbw->timestamp(),
1389  'page_latest' => 0, // Fill this in shortly...
1390  'page_len' => 0, // Fill this in shortly...
1391  ] + $pageIdForInsert,
1392  __METHOD__,
1393  [ 'IGNORE' ]
1394  );
1395 
1396  if ( $dbw->affectedRows() > 0 ) {
1397  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1398  $this->mId = $newid;
1399  $this->mTitle->resetArticleID( $newid );
1400 
1401  return $newid;
1402  } else {
1403  return false; // nothing changed
1404  }
1405  }
1406 
1422  public function updateRevisionOn(
1423  $dbw,
1424  RevisionRecord $revision,
1425  $lastRevision = null,
1426  $lastRevIsRedirect = null
1427  ) {
1428  // TODO: move into PageUpdater or PageStore
1429  // NOTE: when doing that, make sure cached fields get reset in doUserEditContent,
1430  // and in the compat stub!
1431 
1432  // Assertion to try to catch T92046
1433  if ( (int)$revision->getId() === 0 ) {
1434  throw new InvalidArgumentException(
1435  __METHOD__ . ': revision has ID ' . var_export( $revision->getId(), 1 )
1436  );
1437  }
1438 
1439  $content = $revision->getContent( SlotRecord::MAIN );
1440  $len = $content ? $content->getSize() : 0;
1441  $rt = $content ? $content->getRedirectTarget() : null;
1442  $isNew = ( $lastRevision === 0 ) ? 1 : 0;
1443  $isRedirect = $rt !== null ? 1 : 0;
1444 
1445  $conditions = [ 'page_id' => $this->getId() ];
1446 
1447  if ( $lastRevision !== null ) {
1448  // An extra check against threads stepping on each other
1449  $conditions['page_latest'] = $lastRevision;
1450  }
1451 
1452  $revId = $revision->getId();
1453  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1454 
1455  $model = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel();
1456 
1457  $row = [ /* SET */
1458  'page_latest' => $revId,
1459  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1460  'page_is_new' => $isNew,
1461  'page_is_redirect' => $isRedirect,
1462  'page_len' => $len,
1463  'page_content_model' => $model,
1464  ];
1465 
1466  $dbw->update( 'page',
1467  $row,
1468  $conditions,
1469  __METHOD__ );
1470 
1471  $result = $dbw->affectedRows() > 0;
1472  if ( $result ) {
1473  $insertedRow = $this->pageData( $dbw, [ 'page_id' => $this->getId() ] );
1474 
1475  if ( !$insertedRow ) {
1476  throw new MWException( 'Failed to load freshly inserted row' );
1477  }
1478 
1479  $this->mTitle->loadFromRow( $insertedRow );
1480  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1481  $this->setLastEdit( $revision );
1482  $this->mRedirectTarget = null;
1483  $this->mHasRedirectTarget = null;
1484  $this->mPageIsRedirectField = (bool)$rt;
1485  $this->mIsNew = (bool)$isNew;
1486  $this->mIsRedirect = (bool)$isRedirect;
1487 
1488  // Update the LinkCache.
1489  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1490  $linkCache->addGoodLinkObjFromRow(
1491  $this->mTitle,
1492  $insertedRow
1493  );
1494  }
1495 
1496  return $result;
1497  }
1498 
1510  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1511  // Always update redirects (target link might have changed)
1512  // Update/Insert if we don't know if the last revision was a redirect or not
1513  // Delete if changing from redirect to non-redirect
1514  $isRedirect = $redirectTitle !== null;
1515 
1516  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1517  return true;
1518  }
1519 
1520  if ( $isRedirect ) {
1521  $success = $this->insertRedirectEntry( $redirectTitle );
1522  } else {
1523  // This is not a redirect, remove row from redirect table
1524  $where = [ 'rd_from' => $this->getId() ];
1525  $dbw->delete( 'redirect', $where, __METHOD__ );
1526  $success = true;
1527  }
1528 
1529  if ( $this->getTitle()->getNamespace() === NS_FILE ) {
1530  MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
1531  ->invalidateImageRedirect( $this->getTitle() );
1532  }
1533 
1534  return $success;
1535  }
1536 
1550  $aSlots = $a->getSlots();
1551  $bSlots = $b->getSlots();
1552  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1553 
1554  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1555  }
1556 
1567  public function supportsSections() {
1568  return $this->getContentHandler()->supportsSections();
1569  }
1570 
1585  public function replaceSectionContent(
1586  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1587  ) {
1588  $baseRevId = null;
1589  if ( $edittime && $sectionId !== 'new' ) {
1590  $lb = $this->getDBLoadBalancer();
1591  $rev = $this->getRevisionStore()->getRevisionByTimestamp( $this->mTitle, $edittime );
1592  // Try the primary database if this thread may have just added it.
1593  // The logic to fallback to the primary database if the replica is missing
1594  // the revision could be generalized into RevisionStore, but we don't want
1595  // to encourage loading of revisions by timestamp.
1596  if ( !$rev
1597  && $lb->getServerCount() > 1
1598  && $lb->hasOrMadeRecentPrimaryChanges()
1599  ) {
1600  $rev = $this->getRevisionStore()->getRevisionByTimestamp(
1601  $this->mTitle, $edittime, RevisionStore::READ_LATEST );
1602  }
1603  if ( $rev ) {
1604  $baseRevId = $rev->getId();
1605  }
1606  }
1607 
1608  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1609  }
1610 
1624  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1625  $sectionTitle = '', $baseRevId = null
1626  ) {
1627  if ( strval( $sectionId ) === '' ) {
1628  // Whole-page edit; let the whole text through
1629  $newContent = $sectionContent;
1630  } else {
1631  if ( !$this->supportsSections() ) {
1632  throw new MWException( "sections not supported for content model " .
1633  $this->getContentHandler()->getModelID() );
1634  }
1635 
1636  // T32711: always use current version when adding a new section
1637  if ( $baseRevId === null || $sectionId === 'new' ) {
1638  $oldContent = $this->getContent();
1639  } else {
1640  $revRecord = $this->getRevisionStore()->getRevisionById( $baseRevId );
1641  if ( !$revRecord ) {
1642  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1643  $this->getId() . "; section: $sectionId)" );
1644  return null;
1645  }
1646 
1647  $oldContent = $revRecord->getContent( SlotRecord::MAIN );
1648  }
1649 
1650  if ( !$oldContent ) {
1651  wfDebug( __METHOD__ . ": no page text" );
1652  return null;
1653  }
1654 
1655  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1656  }
1657 
1658  return $newContent;
1659  }
1660 
1670  public function checkFlags( $flags ) {
1671  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1672  if ( $this->exists() ) {
1673  $flags |= EDIT_UPDATE;
1674  } else {
1675  $flags |= EDIT_NEW;
1676  }
1677  }
1678 
1679  return $flags;
1680  }
1681 
1709  private function getDerivedDataUpdater(
1710  UserIdentity $forUser = null,
1711  RevisionRecord $forRevision = null,
1712  RevisionSlotsUpdate $forUpdate = null,
1713  $forEdit = false
1714  ) {
1715  if ( !$forRevision && !$forUpdate ) {
1716  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1717  // going to use it with.
1718  $this->derivedDataUpdater = null;
1719  }
1720 
1721  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1722  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1723  // to it did not yet initialize it, because we don't know what data it will be
1724  // initialized with.
1725  $this->derivedDataUpdater = null;
1726  }
1727 
1728  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1729  // However, there is no good way to construct a cache key. We'd need to check against all
1730  // cached instances.
1731 
1732  if ( $this->derivedDataUpdater
1733  && !$this->derivedDataUpdater->isReusableFor(
1734  $forUser,
1735  $forRevision,
1736  $forUpdate,
1737  $forEdit ? $this->getLatest() : null
1738  )
1739  ) {
1740  $this->derivedDataUpdater = null;
1741  }
1742 
1743  if ( !$this->derivedDataUpdater ) {
1744  $this->derivedDataUpdater =
1745  $this->getPageUpdaterFactory()->newDerivedPageDataUpdater( $this );
1746  }
1747 
1749  }
1750 
1771  public function newPageUpdater( $performer, RevisionSlotsUpdate $forUpdate = null ) {
1772  if ( $performer instanceof Authority ) {
1773  // TODO: Deprecate this. But better get rid of this method entirely.
1774  $performer = $performer->getUser();
1775  }
1776 
1777  $pageUpdater = $this->getPageUpdaterFactory()->newPageUpdaterForDerivedPageDataUpdater(
1778  $this,
1779  $performer,
1780  $this->getDerivedDataUpdater( $performer, null, $forUpdate, true )
1781  );
1782 
1783  return $pageUpdater;
1784  }
1785 
1851  public function doEditContent(
1852  Content $content, $summary, $flags = 0, $originalRevId = false,
1853  Authority $performer = null, $serialFormat = null, $tags = [], $undidRevId = 0
1854  ) {
1855  wfDeprecated( __METHOD__, '1.32' );
1856 
1857  if ( !$performer ) {
1858  // Its okay to fallback to $wgUser because this whole method is deprecated
1859  // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser
1860  global $wgUser;
1861  $performer = StubGlobalUser::getRealUser( $wgUser );
1862  }
1863 
1864  return $this->doUserEditContent(
1865  $content, $performer, $summary, $flags, $originalRevId, $tags, $undidRevId
1866  );
1867  }
1868 
1929  public function doUserEditContent(
1930  Content $content,
1931  Authority $performer,
1932  $summary,
1933  $flags = 0,
1934  $originalRevId = false,
1935  $tags = [],
1936  $undidRevId = 0
1937  ) {
1938  $useNPPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( 'UseNPPatrol' );
1939  $useRCPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( 'UseRCPatrol' );
1940  if ( !( $summary instanceof CommentStoreComment ) ) {
1941  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1942  }
1943 
1944  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1945  // Checking the minoredit right should be done in the same place the 'bot' right is
1946  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1947  if ( ( $flags & EDIT_MINOR ) && !$performer->isAllowed( 'minoredit' ) ) {
1948  $flags &= ~EDIT_MINOR;
1949  }
1950 
1951  $slotsUpdate = new RevisionSlotsUpdate();
1952  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1953 
1954  // NOTE: while doUserEditContent() executes, callbacks to getDerivedDataUpdater and
1955  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1956  // used by this PageUpdater. However, there is no guarantee for this.
1957  $updater = $this->newPageUpdater( $performer, $slotsUpdate )
1958  ->setContent( SlotRecord::MAIN, $content )
1959  ->setOriginalRevisionId( $originalRevId );
1960  if ( $undidRevId ) {
1961  $updater->markAsRevert(
1962  EditResult::REVERT_UNDO,
1963  $undidRevId,
1964  $originalRevId ?: null
1965  );
1966  }
1967 
1968  $needsPatrol = $useRCPatrol || ( $useNPPatrol && !$this->exists() );
1969 
1970  // TODO: this logic should not be in the storage layer, it's here for compatibility
1971  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1972  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1973 
1974  if ( $needsPatrol && $performer->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
1975  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1976  }
1977 
1978  $updater->addTags( $tags );
1979 
1980  $revRec = $updater->saveRevision(
1981  $summary,
1982  $flags
1983  );
1984 
1985  // $revRec will be null if the edit failed, or if no new revision was created because
1986  // the content did not change.
1987  if ( $revRec ) {
1988  // update cached fields
1989  // TODO: this is currently redundant to what is done in updateRevisionOn.
1990  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1991  $this->setLastEdit( $revRec );
1992  }
1993 
1994  return $updater->getStatus();
1995  }
1996 
2011  public function makeParserOptions( $context ) {
2012  $options = ParserOptions::newCanonical( $context );
2013 
2014  if ( $this->getTitle()->isConversionTable() ) {
2015  // @todo ConversionTable should become a separate content model, so
2016  // we don't need special cases like this one.
2017  $options->disableContentConversion();
2018  }
2019 
2020  return $options;
2021  }
2022 
2042  public function prepareContentForEdit(
2043  Content $content,
2044  RevisionRecord $revision = null,
2045  UserIdentity $user = null,
2046  $serialFormat = null,
2047  $useStash = true
2048  ) {
2049  if ( !$user ) {
2050  wfDeprecated( __METHOD__ . ' without a UserIdentity', '1.37' );
2051  // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser
2052  global $wgUser;
2053  $user = StubGlobalUser::getRealUser( $wgUser );
2054  }
2055 
2056  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2057  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2058 
2059  if ( !$updater->isUpdatePrepared() ) {
2060  $updater->prepareContent( $user, $slots, $useStash );
2061 
2062  if ( $revision ) {
2063  $updater->prepareUpdate(
2064  $revision,
2065  [
2066  'causeAction' => 'prepare-edit',
2067  'causeAgent' => $user->getName(),
2068  ]
2069  );
2070  }
2071  }
2072 
2073  return $updater->getPreparedEdit();
2074  }
2075 
2104  public function doEditUpdates(
2105  RevisionRecord $revisionRecord,
2106  UserIdentity $user,
2107  array $options = []
2108  ) {
2109  $options += [
2110  'causeAction' => 'edit-page',
2111  'causeAgent' => $user->getName(),
2112  ];
2113 
2114  $updater = $this->getDerivedDataUpdater( $user, $revisionRecord );
2115 
2116  $updater->prepareUpdate( $revisionRecord, $options );
2117 
2118  $updater->doUpdates();
2119  }
2120 
2134  public function updateParserCache( array $options = [] ) {
2135  $revision = $this->getRevisionRecord();
2136  if ( !$revision || !$revision->getId() ) {
2137  LoggerFactory::getInstance( 'wikipage' )->info(
2138  __METHOD__ . ' called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2139  );
2140  return;
2141  }
2142  $userIdentity = $revision->getUser( RevisionRecord::RAW );
2143 
2144  $updater = $this->getDerivedDataUpdater( $userIdentity, $revision );
2145  $updater->prepareUpdate( $revision, $options );
2146  $updater->doParserCacheUpdate();
2147  }
2148 
2178  public function doSecondaryDataUpdates( array $options = [] ) {
2179  $options['recursive'] = $options['recursive'] ?? true;
2180  $revision = $this->getRevisionRecord();
2181  if ( !$revision || !$revision->getId() ) {
2182  LoggerFactory::getInstance( 'wikipage' )->info(
2183  __METHOD__ . ' called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2184  );
2185  return;
2186  }
2187  $userIdentity = $revision->getUser( RevisionRecord::RAW );
2188 
2189  $updater = $this->getDerivedDataUpdater( $userIdentity, $revision );
2190  $updater->prepareUpdate( $revision, $options );
2191  $updater->doSecondaryDataUpdates( $options );
2192  }
2193 
2208  public function doUpdateRestrictions( array $limit, array $expiry,
2209  &$cascade, $reason, UserIdentity $user, $tags = null
2210  ) {
2212 
2213  if ( wfReadOnly() ) {
2214  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2215  }
2216 
2217  $this->loadPageData( 'fromdbmaster' );
2218  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2219  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2220  $id = $this->getId();
2221 
2222  if ( !$cascade ) {
2223  $cascade = false;
2224  }
2225 
2226  // Take this opportunity to purge out expired restrictions
2228 
2229  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2230  // we expect a single selection, but the schema allows otherwise.
2231  $isProtected = false;
2232  $protect = false;
2233  $changed = false;
2234 
2235  $dbw = wfGetDB( DB_PRIMARY );
2236 
2237  foreach ( $restrictionTypes as $action ) {
2238  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2239  $expiry[$action] = 'infinity';
2240  }
2241  if ( !isset( $limit[$action] ) ) {
2242  $limit[$action] = '';
2243  } elseif ( $limit[$action] != '' ) {
2244  $protect = true;
2245  }
2246 
2247  // Get current restrictions on $action
2248  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2249  if ( $current != '' ) {
2250  $isProtected = true;
2251  }
2252 
2253  if ( $limit[$action] != $current ) {
2254  $changed = true;
2255  } elseif ( $limit[$action] != '' ) {
2256  // Only check expiry change if the action is actually being
2257  // protected, since expiry does nothing on an not-protected
2258  // action.
2259  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2260  $changed = true;
2261  }
2262  }
2263  }
2264 
2265  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2266  $changed = true;
2267  }
2268 
2269  // If nothing has changed, do nothing
2270  if ( !$changed ) {
2271  return Status::newGood();
2272  }
2273 
2274  if ( !$protect ) { // No protection at all means unprotection
2275  $revCommentMsg = 'unprotectedarticle-comment';
2276  $logAction = 'unprotect';
2277  } elseif ( $isProtected ) {
2278  $revCommentMsg = 'modifiedarticleprotection-comment';
2279  $logAction = 'modify';
2280  } else {
2281  $revCommentMsg = 'protectedarticle-comment';
2282  $logAction = 'protect';
2283  }
2284 
2285  $logRelationsValues = [];
2286  $logRelationsField = null;
2287  $logParamsDetails = [];
2288 
2289  // Null revision (used for change tag insertion)
2290  $nullRevisionRecord = null;
2291 
2292  if ( $id ) { // Protection of existing page
2293  $legacyUser = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user );
2294  if ( !$this->getHookRunner()->onArticleProtect( $this, $legacyUser, $limit, $reason ) ) {
2295  return Status::newGood();
2296  }
2297 
2298  // Only certain restrictions can cascade...
2299  $editrestriction = isset( $limit['edit'] )
2300  ? [ $limit['edit'] ]
2301  : $this->mTitle->getRestrictions( 'edit' );
2302  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2303  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2304  }
2305  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2306  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2307  }
2308 
2309  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2310  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2311  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2312  }
2313  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2314  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2315  }
2316 
2317  // The schema allows multiple restrictions
2318  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2319  $cascade = false;
2320  }
2321 
2322  // insert null revision to identify the page protection change as edit summary
2323  $latest = $this->getLatest();
2324  $nullRevisionRecord = $this->insertNullProtectionRevision(
2325  $revCommentMsg,
2326  $limit,
2327  $expiry,
2328  $cascade,
2329  $reason,
2330  $user
2331  );
2332 
2333  if ( $nullRevisionRecord === null ) {
2334  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2335  }
2336 
2337  $logRelationsField = 'pr_id';
2338 
2339  // T214035: Avoid deadlock on MySQL.
2340  // Do a DELETE by primary key (pr_id) for any existing protection rows.
2341  // On MySQL and derivatives, unconditionally deleting by page ID (pr_page) would.
2342  // place a gap lock if there are no matching rows. This can deadlock when another
2343  // thread modifies protection settings for page IDs in the same gap.
2344  $existingProtectionIds = $dbw->selectFieldValues(
2345  'page_restrictions',
2346  'pr_id',
2347  [
2348  'pr_page' => $id,
2349  'pr_type' => array_map( 'strval', array_keys( $limit ) )
2350  ],
2351  __METHOD__
2352  );
2353 
2354  if ( $existingProtectionIds ) {
2355  $dbw->delete(
2356  'page_restrictions',
2357  [ 'pr_id' => $existingProtectionIds ],
2358  __METHOD__
2359  );
2360  }
2361 
2362  // Update restrictions table
2363  foreach ( $limit as $action => $restrictions ) {
2364  if ( $restrictions != '' ) {
2365  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2366  $dbw->insert(
2367  'page_restrictions',
2368  [
2369  'pr_page' => $id,
2370  'pr_type' => $action,
2371  'pr_level' => $restrictions,
2372  'pr_cascade' => $cascadeValue,
2373  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2374  ],
2375  __METHOD__
2376  );
2377  $logRelationsValues[] = $dbw->insertId();
2378  $logParamsDetails[] = [
2379  'type' => $action,
2380  'level' => $restrictions,
2381  'expiry' => $expiry[$action],
2382  'cascade' => (bool)$cascadeValue,
2383  ];
2384  }
2385  }
2386 
2387  // Clear out legacy restriction fields
2388  $dbw->update(
2389  'page',
2390  [ 'page_restrictions' => '' ],
2391  [ 'page_id' => $id ],
2392  __METHOD__
2393  );
2394 
2395  $this->getHookRunner()->onRevisionFromEditComplete(
2396  $this, $nullRevisionRecord, $latest, $user, $tags );
2397 
2398  $this->getHookRunner()->onArticleProtectComplete( $this, $legacyUser, $limit, $reason );
2399  } else { // Protection of non-existing page (also known as "title protection")
2400  // Cascade protection is meaningless in this case
2401  $cascade = false;
2402 
2403  if ( $limit['create'] != '' ) {
2404  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2405  $dbw->replace( 'protected_titles',
2406  [ [ 'pt_namespace', 'pt_title' ] ],
2407  [
2408  'pt_namespace' => $this->mTitle->getNamespace(),
2409  'pt_title' => $this->mTitle->getDBkey(),
2410  'pt_create_perm' => $limit['create'],
2411  'pt_timestamp' => $dbw->timestamp(),
2412  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2413  'pt_user' => $user->getId(),
2414  ] + $commentFields, __METHOD__
2415  );
2416  $logParamsDetails[] = [
2417  'type' => 'create',
2418  'level' => $limit['create'],
2419  'expiry' => $expiry['create'],
2420  ];
2421  } else {
2422  $dbw->delete( 'protected_titles',
2423  [
2424  'pt_namespace' => $this->mTitle->getNamespace(),
2425  'pt_title' => $this->mTitle->getDBkey()
2426  ], __METHOD__
2427  );
2428  }
2429  }
2430 
2431  $this->mTitle->flushRestrictions();
2432  InfoAction::invalidateCache( $this->mTitle );
2433 
2434  if ( $logAction == 'unprotect' ) {
2435  $params = [];
2436  } else {
2437  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2438  $params = [
2439  '4::description' => $protectDescriptionLog, // parameter for IRC
2440  '5:bool:cascade' => $cascade,
2441  'details' => $logParamsDetails, // parameter for localize and api
2442  ];
2443  }
2444 
2445  // Update the protection log
2446  $logEntry = new ManualLogEntry( 'protect', $logAction );
2447  $logEntry->setTarget( $this->mTitle );
2448  $logEntry->setComment( $reason );
2449  $logEntry->setPerformer( $user );
2450  $logEntry->setParameters( $params );
2451  if ( $nullRevisionRecord !== null ) {
2452  $logEntry->setAssociatedRevId( $nullRevisionRecord->getId() );
2453  }
2454  $logEntry->addTags( $tags );
2455  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2456  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2457  }
2458  $logId = $logEntry->insert();
2459  $logEntry->publish( $logId );
2460 
2461  return Status::newGood( $logId );
2462  }
2463 
2484  public function getCurrentUpdate(): PreparedUpdate {
2485  Assert::precondition(
2486  $this->derivedDataUpdater !== null,
2487  'There is no ongoing update tracked by this instance of WikiPage!'
2488  );
2489 
2491  }
2492 
2507  string $revCommentMsg,
2508  array $limit,
2509  array $expiry,
2510  bool $cascade,
2511  string $reason,
2512  UserIdentity $user
2513  ): ?RevisionRecord {
2514  $dbw = wfGetDB( DB_PRIMARY );
2515 
2516  // Prepare a null revision to be added to the history
2517  $editComment = wfMessage(
2518  $revCommentMsg,
2519  $this->mTitle->getPrefixedText(),
2520  $user->getName()
2521  )->inContentLanguage()->text();
2522  if ( $reason ) {
2523  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2524  }
2525  $protectDescription = $this->protectDescription( $limit, $expiry );
2526  if ( $protectDescription ) {
2527  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2528  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2529  ->inContentLanguage()->text();
2530  }
2531  if ( $cascade ) {
2532  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2533  $editComment .= wfMessage( 'brackets' )->params(
2534  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2535  )->inContentLanguage()->text();
2536  }
2537 
2538  $revStore = $this->getRevisionStore();
2539  $comment = CommentStoreComment::newUnsavedComment( $editComment );
2540  $nullRevRecord = $revStore->newNullRevision(
2541  $dbw,
2542  $this->getTitle(),
2543  $comment,
2544  true,
2545  $user
2546  );
2547 
2548  if ( $nullRevRecord ) {
2549  $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
2550 
2551  // Update page record and touch page
2552  $oldLatest = $inserted->getParentId();
2553 
2554  $this->updateRevisionOn( $dbw, $inserted, $oldLatest );
2555 
2556  return $inserted;
2557  } else {
2558  return null;
2559  }
2560  }
2561 
2566  protected function formatExpiry( $expiry ) {
2567  if ( $expiry != 'infinity' ) {
2568  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2569  return wfMessage(
2570  'protect-expiring',
2571  $contLang->timeanddate( $expiry, false, false ),
2572  $contLang->date( $expiry, false, false ),
2573  $contLang->time( $expiry, false, false )
2574  )->inContentLanguage()->text();
2575  } else {
2576  return wfMessage( 'protect-expiry-indefinite' )
2577  ->inContentLanguage()->text();
2578  }
2579  }
2580 
2588  public function protectDescription( array $limit, array $expiry ) {
2589  $protectDescription = '';
2590 
2591  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2592  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2593  # All possible message keys are listed here for easier grepping:
2594  # * restriction-create
2595  # * restriction-edit
2596  # * restriction-move
2597  # * restriction-upload
2598  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2599  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2600  # with '' filtered out. All possible message keys are listed below:
2601  # * protect-level-autoconfirmed
2602  # * protect-level-sysop
2603  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2604  ->inContentLanguage()->text();
2605 
2606  $expiryText = $this->formatExpiry( $expiry[$action] );
2607 
2608  if ( $protectDescription !== '' ) {
2609  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2610  }
2611  $protectDescription .= wfMessage( 'protect-summary-desc' )
2612  ->params( $actionText, $restrictionsText, $expiryText )
2613  ->inContentLanguage()->text();
2614  }
2615 
2616  return $protectDescription;
2617  }
2618 
2630  public function protectDescriptionLog( array $limit, array $expiry ) {
2631  $protectDescriptionLog = '';
2632 
2633  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2634  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2635  $expiryText = $this->formatExpiry( $expiry[$action] );
2636  $protectDescriptionLog .=
2637  $dirMark .
2638  "[$action=$restrictions] ($expiryText)";
2639  }
2640 
2641  return trim( $protectDescriptionLog );
2642  }
2643 
2658  public function isBatchedDelete( $safetyMargin = 0 ) {
2659  $deleteRevisionsBatchSize = MediaWikiServices::getInstance()
2660  ->getMainConfig()->get( 'DeleteRevisionsBatchSize' );
2661 
2662  $dbr = wfGetDB( DB_REPLICA );
2663  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2664  $revCount += $safetyMargin;
2665 
2666  return $revCount >= $deleteRevisionsBatchSize;
2667  }
2668 
2699  public function doDeleteArticleReal(
2700  $reason, UserIdentity $deleter, $suppress = false, $u1 = null, &$error = '', $u2 = null,
2701  $tags = [], $logsubtype = 'delete', $immediate = false
2702  ) {
2703  $services = MediaWikiServices::getInstance();
2704  $deletePage = $services->getDeletePageFactory()->newDeletePage(
2705  $this,
2706  $services->getUserFactory()->newFromUserIdentity( $deleter )
2707  );
2708 
2709  $status = $deletePage
2710  ->setSuppress( $suppress )
2711  ->setTags( $tags ?: [] )
2712  ->setLogSubtype( $logsubtype )
2713  ->forceImmediate( $immediate )
2714  ->keepLegacyHookErrorsSeparate()
2715  ->deleteUnsafe( $reason );
2716  $error = $deletePage->getLegacyHookErrors();
2717  if ( $status->isGood() ) {
2718  // BC with old return format
2719  if ( $deletePage->deletionsWereScheduled()[DeletePage::PAGE_BASE] ) {
2720  $status->warning( 'delete-scheduled', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2721  } else {
2722  $status->value = $deletePage->getSuccessfulDeletionsIDs()[DeletePage::PAGE_BASE];
2723  }
2724  }
2725  return $status;
2726  }
2727 
2746  public function doDeleteArticleBatched(
2747  $reason, $suppress, UserIdentity $deleter, $tags,
2748  $logsubtype, $immediate = false, $webRequestId = null
2749  ) {
2750  $services = MediaWikiServices::getInstance();
2751  $deletePage = $services->getDeletePageFactory()->newDeletePage(
2752  $this,
2753  $services->getUserFactory()->newFromUserIdentity( $deleter )
2754  );
2755 
2756  $status = $deletePage
2757  ->setSuppress( $suppress )
2758  ->setTags( $tags )
2759  ->setLogSubtype( $logsubtype )
2760  ->forceImmediate( $immediate )
2761  ->deleteInternal( $this, DeletePage::PAGE_BASE, $reason, $webRequestId );
2762  if ( $status->isGood() ) {
2763  // BC with old return format
2764  if ( $deletePage->deletionsWereScheduled()[DeletePage::PAGE_BASE] ) {
2765  $status->warning( 'delete-scheduled', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2766  } else {
2767  $status->value = $deletePage->getSuccessfulDeletionsIDs()[DeletePage::PAGE_BASE];
2768  }
2769  }
2770  return $status;
2771  }
2772 
2779  public function lockAndGetLatest() {
2780  return (int)wfGetDB( DB_PRIMARY )->selectField(
2781  'page',
2782  'page_latest',
2783  [
2784  'page_id' => $this->getId(),
2785  // Typically page_id is enough, but some code might try to do
2786  // updates assuming the title is the same, so verify that
2787  'page_namespace' => $this->getTitle()->getNamespace(),
2788  'page_title' => $this->getTitle()->getDBkey()
2789  ],
2790  __METHOD__,
2791  [ 'FOR UPDATE' ]
2792  );
2793  }
2794 
2809  public function doDeleteUpdates(
2810  $id,
2811  Content $content = null,
2812  RevisionRecord $revRecord = null,
2813  UserIdentity $user = null
2814  ) {
2815  wfDeprecated( __METHOD__, '1.37' );
2816  if ( !$revRecord ) {
2817  throw new BadMethodCallException( __METHOD__ . ' now requires a RevisionRecord' );
2818  }
2819  if ( $id !== $this->getId() ) {
2820  throw new InvalidArgumentException( 'Mismatching page ID' );
2821  }
2822 
2823  $user = $user ?? new UserIdentityValue( 0, 'unknown' );
2824  $services = MediaWikiServices::getInstance();
2825  $deletePage = $services->getDeletePageFactory()->newDeletePage(
2826  $this,
2827  $services->getUserFactory()->newFromUserIdentity( $user )
2828  );
2829 
2830  $deletePage->doDeleteUpdates( $this, $revRecord );
2831  }
2832 
2844  public static function onArticleCreate( Title $title ) {
2845  // TODO: move this into a PageEventEmitter service
2846 
2847  // Update existence markers on article/talk tabs...
2848  $other = $title->getOtherPage();
2849 
2850  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2851  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2852 
2853  $title->touchLinks();
2854  $title->deleteTitleProtection();
2855 
2856  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
2857 
2858  // Invalidate caches of articles which include this page
2860  $title,
2861  'templatelinks',
2862  [ 'causeAction' => 'page-create' ]
2863  );
2864  JobQueueGroup::singleton()->lazyPush( $job );
2865 
2866  if ( $title->getNamespace() === NS_CATEGORY ) {
2867  // Load the Category object, which will schedule a job to create
2868  // the category table row if necessary. Checking a replica DB is ok
2869  // here, in the worst case it'll run an unnecessary recount job on
2870  // a category that probably doesn't have many members.
2871  Category::newFromTitle( $title )->getID();
2872  }
2873  }
2874 
2880  public static function onArticleDelete( Title $title ) {
2881  // TODO: move this into a PageEventEmitter service
2882 
2883  // Update existence markers on article/talk tabs...
2884  $other = $title->getOtherPage();
2885 
2886  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2887  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2888 
2889  $title->touchLinks();
2890 
2891  $services = MediaWikiServices::getInstance();
2892  $services->getLinkCache()->invalidateTitle( $title );
2893 
2895 
2896  // Messages
2897  if ( $title->getNamespace() === NS_MEDIAWIKI ) {
2898  $services->getMessageCache()->updateMessageOverride( $title, null );
2899  }
2900 
2901  // Images
2902  if ( $title->getNamespace() === NS_FILE ) {
2904  $title,
2905  'imagelinks',
2906  [ 'causeAction' => 'page-delete' ]
2907  );
2908  JobQueueGroup::singleton()->lazyPush( $job );
2909  }
2910 
2911  // User talk pages
2912  if ( $title->getNamespace() === NS_USER_TALK ) {
2913  $user = User::newFromName( $title->getText(), false );
2914  if ( $user ) {
2915  MediaWikiServices::getInstance()
2916  ->getTalkPageNotificationManager()
2917  ->removeUserHasNewMessages( $user );
2918  }
2919  }
2920 
2921  // Image redirects
2922  $services->getRepoGroup()->getLocalRepo()->invalidateImageRedirect( $title );
2923 
2924  // Purge cross-wiki cache entities referencing this page
2926  }
2927 
2936  public static function onArticleEdit(
2937  Title $title,
2938  RevisionRecord $revRecord = null,
2939  $slotsChanged = null
2940  ) {
2941  // TODO: move this into a PageEventEmitter service
2942 
2943  $jobs = [];
2944  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
2945  // Invalidate caches of articles which include this page.
2946  // Only for the main slot, because only the main slot is transcluded.
2947  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
2949  $title,
2950  'templatelinks',
2951  [ 'causeAction' => 'page-edit' ]
2952  );
2953  }
2954  // Invalidate the caches of all pages which redirect here
2956  $title,
2957  'redirect',
2958  [ 'causeAction' => 'page-edit' ]
2959  );
2960  JobQueueGroup::singleton()->lazyPush( $jobs );
2961 
2962  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
2963 
2964  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2965  $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2966 
2967  // Purge ?action=info cache
2968  $revid = $revRecord ? $revRecord->getId() : null;
2969  DeferredUpdates::addCallableUpdate( static function () use ( $title, $revid ) {
2971  } );
2972 
2973  // Purge cross-wiki cache entities referencing this page
2975  }
2976 
2984  private static function purgeInterwikiCheckKey( Title $title ) {
2985  $enableScaryTranscluding = MediaWikiServices::getInstance()->getMainConfig()->get( 'EnableScaryTranscluding' );
2986 
2987  if ( !$enableScaryTranscluding ) {
2988  return; // @todo: perhaps this wiki is only used as a *source* for content?
2989  }
2990 
2991  DeferredUpdates::addCallableUpdate( static function () use ( $title ) {
2992  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2993  $cache->resetCheckKey(
2994  // Do not include the namespace since there can be multiple aliases to it
2995  // due to different namespace text definitions on different wikis. This only
2996  // means that some cache invalidations happen that are not strictly needed.
2997  $cache->makeGlobalKey(
2998  'interwiki-page',
3000  $title->getDBkey()
3001  )
3002  );
3003  } );
3004  }
3005 
3012  public function getCategories() {
3013  $id = $this->getId();
3014  if ( $id == 0 ) {
3015  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3016  }
3017 
3018  $dbr = wfGetDB( DB_REPLICA );
3019  $res = $dbr->select( 'categorylinks',
3020  [ 'page_title' => 'cl_to', 'page_namespace' => NS_CATEGORY ],
3021  [ 'cl_from' => $id ],
3022  __METHOD__
3023  );
3024 
3025  return TitleArray::newFromResult( $res );
3026  }
3027 
3034  public function getHiddenCategories() {
3035  $result = [];
3036  $id = $this->getId();
3037 
3038  if ( $id == 0 ) {
3039  return [];
3040  }
3041 
3042  $dbr = wfGetDB( DB_REPLICA );
3043  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3044  [ 'cl_to' ],
3045  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3046  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3047  __METHOD__ );
3048 
3049  if ( $res !== false ) {
3050  foreach ( $res as $row ) {
3051  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3052  }
3053  }
3054 
3055  return $result;
3056  }
3057 
3065  public function getAutoDeleteReason( &$hasHistory = false ) {
3066  if ( func_num_args() === 1 ) {
3067  wfDeprecated( __METHOD__ . ': $hasHistory parameter', '1.38' );
3068  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3069  }
3070  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle() );
3071  }
3072 
3083  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3084  $id = $id ?: $this->getId();
3085  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3086  getCategoryLinkType( $this->getTitle()->getNamespace() );
3087 
3088  $addFields = [ 'cat_pages = cat_pages + 1' ];
3089  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3090  if ( $type !== 'page' ) {
3091  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3092  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3093  }
3094 
3095  $dbw = wfGetDB( DB_PRIMARY );
3096 
3097  if ( count( $added ) ) {
3098  $existingAdded = $dbw->selectFieldValues(
3099  'category',
3100  'cat_title',
3101  [ 'cat_title' => $added ],
3102  __METHOD__
3103  );
3104 
3105  // For category rows that already exist, do a plain
3106  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3107  // to avoid creating gaps in the cat_id sequence.
3108  if ( count( $existingAdded ) ) {
3109  $dbw->update(
3110  'category',
3111  $addFields,
3112  [ 'cat_title' => $existingAdded ],
3113  __METHOD__
3114  );
3115  }
3116 
3117  $missingAdded = array_diff( $added, $existingAdded );
3118  if ( count( $missingAdded ) ) {
3119  $insertRows = [];
3120  foreach ( $missingAdded as $cat ) {
3121  $insertRows[] = [
3122  'cat_title' => $cat,
3123  'cat_pages' => 1,
3124  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3125  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3126  ];
3127  }
3128  $dbw->upsert(
3129  'category',
3130  $insertRows,
3131  'cat_title',
3132  $addFields,
3133  __METHOD__
3134  );
3135  }
3136  }
3137 
3138  if ( count( $deleted ) ) {
3139  $dbw->update(
3140  'category',
3141  $removeFields,
3142  [ 'cat_title' => $deleted ],
3143  __METHOD__
3144  );
3145  }
3146 
3147  foreach ( $added as $catName ) {
3148  $cat = Category::newFromName( $catName );
3149  $this->getHookRunner()->onCategoryAfterPageAdded( $cat, $this );
3150  }
3151 
3152  foreach ( $deleted as $catName ) {
3153  $cat = Category::newFromName( $catName );
3154  $this->getHookRunner()->onCategoryAfterPageRemoved( $cat, $this, $id );
3155  // Refresh counts on categories that should be empty now (after commit, T166757)
3156  DeferredUpdates::addCallableUpdate( static function () use ( $cat ) {
3157  $cat->refreshCountsIfEmpty();
3158  } );
3159  }
3160  }
3161 
3174  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3175  if ( wfReadOnly() ) {
3176  return;
3177  }
3178 
3179  if ( !$this->getHookRunner()->onOpportunisticLinksUpdate( $this,
3180  $this->mTitle, $parserOutput )
3181  ) {
3182  return;
3183  }
3184 
3185  $config = RequestContext::getMain()->getConfig();
3186 
3187  $params = [
3188  'isOpportunistic' => true,
3189  'rootJobTimestamp' => $parserOutput->getCacheTime()
3190  ];
3191 
3192  if ( $this->mTitle->areRestrictionsCascading() ) {
3193  // In general, MediaWiki does not re-run LinkUpdate (e.g. for search index, category
3194  // listings, and backlinks for Whatlinkshere), unless either the page was directly
3195  // edited, or was re-generate following a template edit propagating to an affected
3196  // page. As such, during page views when there is no valid ParserCache entry,
3197  // we re-parse and save, but leave indexes as-is.
3198  //
3199  // We make an exception for pages that have cascading protection (perhaps for a wiki's
3200  // "Main Page"). When such page is re-parsed on-demand after a parser cache miss, we
3201  // queue a high-priority LinksUpdate job, to ensure that we really protect all
3202  // content that is currently transcluded onto the page. This is important, because
3203  // wikitext supports conditional statements based on the current time, which enables
3204  // transcluding of a different sub page based on which day it is, and then show that
3205  // information on the Main Page, without the Main Page itself being edited.
3206  JobQueueGroup::singleton()->lazyPush(
3207  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3208  );
3209  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasReducedExpiry() ) {
3210  // Assume the output contains "dynamic" time/random based magic words.
3211  // Only update pages that expired due to dynamic content and NOT due to edits
3212  // to referenced templates/files. When the cache expires due to dynamic content,
3213  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3214  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3215  // template/file edit already triggered recursive RefreshLinksJob jobs.
3216  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3217  // If a page is uncacheable, do not keep spamming a job for it.
3218  // Although it would be de-duplicated, it would still waste I/O.
3220  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3221  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3222  if ( $cache->add( $key, time(), $ttl ) ) {
3223  JobQueueGroup::singleton()->lazyPush(
3224  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3225  );
3226  }
3227  }
3228  }
3229  }
3230 
3242  public function getDeletionUpdates( $rev = null ) {
3243  wfDeprecated( __METHOD__, '1.37' );
3244  $user = new UserIdentityValue( 0, 'Legacy code hater' );
3245  $services = MediaWikiServices::getInstance();
3246  $deletePage = $services->getDeletePageFactory()->newDeletePage(
3247  $this,
3248  $services->getUserFactory()->newFromUserIdentity( $user )
3249  );
3250 
3251  if ( !$rev ) {
3252  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3253 
3254  try {
3255  $rev = $this->getRevisionRecord();
3256  } catch ( Exception $ex ) {
3257  // If we can't load the content, something is wrong. Perhaps that's why
3258  // the user is trying to delete the page, so let's not fail in that case.
3259  // Note that doDeleteArticleReal() will already have logged an issue with
3260  // loading the content.
3261  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3262  }
3263  }
3264  if ( !$rev ) {
3265  // Use an empty RevisionRecord
3266  $newRev = new MutableRevisionRecord( $this );
3267  } elseif ( $rev instanceof Content ) {
3268  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3269  $newRev = new MutableRevisionRecord( $this );
3270  $newRev->setSlot( SlotRecord::newUnsaved( SlotRecord::MAIN, $rev ) );
3271  } else {
3272  $newRev = $rev;
3273  }
3274  return $deletePage->getDeletionUpdates( $this, $newRev );
3275  }
3276 
3284  public function isLocal() {
3285  return true;
3286  }
3287 
3297  public function getWikiDisplayName() {
3298  $sitename = MediaWikiServices::getInstance()->getMainConfig()->get( 'Sitename' );
3299  return $sitename;
3300  }
3301 
3310  public function getSourceURL() {
3311  return $this->getTitle()->getCanonicalURL();
3312  }
3313 
3320  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3321 
3322  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3323  }
3324 
3331  public function __wakeup() {
3332  // Make sure we re-fetch the latest state from the database.
3333  // In particular, the latest revision may have changed.
3334  // As a side-effect, this makes sure mLastRevision doesn't
3335  // end up being an instance of the old Revision class (see T259181),
3336  // especially since that class was removed entirely in 1.37.
3337  $this->clear();
3338  }
3339 
3344  public function getNamespace(): int {
3345  return $this->getTitle()->getNamespace();
3346  }
3347 
3352  public function getDBkey(): string {
3353  return $this->getTitle()->getDBkey();
3354  }
3355 
3360  public function getWikiId() {
3361  return $this->getTitle()->getWikiId();
3362  }
3363 
3368  public function canExist(): bool {
3369  return true;
3370  }
3371 
3376  public function __toString(): string {
3377  return $this->mTitle->__toString();
3378  }
3379 
3387  public function isSamePageAs( PageReference $other ): bool {
3388  // NOTE: keep in sync with PageIdentityValue::isSamePageAs()!
3389 
3390  if ( $other->getWikiId() !== $this->getWikiId() ) {
3391  return false;
3392  }
3393 
3394  if ( $other->getNamespace() !== $this->getNamespace()
3395  || $other->getDBkey() !== $this->getDBkey() ) {
3396  return false;
3397  }
3398 
3399  return true;
3400  }
3401 
3413  public function toPageRecord(): ExistingPageRecord {
3414  // TODO: replace individual member fields with a PageRecord instance that is always present
3415 
3416  if ( !$this->mDataLoaded ) {
3417  $this->loadPageData();
3418  }
3419 
3420  Assert::precondition(
3421  $this->exists(),
3422  'This WikiPage instance does not represent an existing page: ' . $this->mTitle
3423  );
3424 
3425  return new PageStoreRecord(
3426  (object)[
3427  'page_id' => $this->getId(),
3428  'page_namespace' => $this->mTitle->getNamespace(),
3429  'page_title' => $this->mTitle->getDBkey(),
3430  'page_latest' => $this->mLatest,
3431  'page_is_new' => $this->mIsNew,
3432  'page_is_redirect' => $this->mIsRedirect,
3433  'page_touched' => $this->getTouched(),
3434  'page_lang' => $this->getLanguage()
3435  ],
3436  PageIdentity::LOCAL
3437  );
3438  }
3439 
3440 }
WikiPage\getCategories
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:3012
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:35
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
WikiPage\getPageIsRedirectField
getPageIsRedirectField()
Get the value of the page_is_redirect field in the DB.
Definition: WikiPage.php:641
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:45
WikiPage\toPageRecord
toPageRecord()
Returns the page represented by this WikiPage as a PageStoreRecord.
Definition: WikiPage.php:3413
MediaWiki\DAO\WikiAwareEntityTrait
trait WikiAwareEntityTrait
Definition: WikiAwareEntityTrait.php:32
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
Page
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:29
CacheTime\getCacheExpiry
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:143
Page\PageRecord
Data record representing a page that is (or used to be, or could be) an editable page on a wiki.
Definition: PageRecord.php:25
MediaWiki\Revision\RevisionRecord\getContent
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
Definition: RevisionRecord.php:156
MediaWiki\Linker\LinkTarget\getInterwiki
getInterwiki()
The interwiki component of this LinkTarget.
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:636
WikiPage\doUserEditContent
doUserEditContent(Content $content, Authority $performer, $summary, $flags=0, $originalRevId=false, $tags=[], $undidRevId=0)
Change an existing article or create a new article.
Definition: WikiPage.php:1929
WikiPage\onArticleCreate
static onArticleCreate(Title $title)
The onArticle*() functions are supposed to be a kind of hooks which should be called whenever any of ...
Definition: WikiPage.php:2844
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
WikiPage\loadPageData
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:469
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
StubGlobalUser\getRealUser
static getRealUser( $globalUser)
Get the relevant "real" user object based on either a User object or a StubGlobalUser wrapper.
Definition: StubGlobalUser.php:100
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:819
ParserOutput
Definition: ParserOutput.php:35
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:72
WikiPage\getRedirectTarget
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:1033
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:273
WikiPage\getComment
getComment( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:940
WikiPage\getWikiId
getWikiId()
Definition: WikiPage.php:3360
WikiPage\clearCacheFields
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:333
WikiPage\isBatchedDelete
isBatchedDelete( $safetyMargin=0)
Determines if deletion of this page would be batched (executed over time by the job queue) or not (co...
Definition: WikiPage.php:2658
WikiPage\wasLoadedFrom
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:514
TitleArray\newFromResult
static newFromResult( $res)
Definition: TitleArray.php:44
Category\newFromTitle
static newFromTitle(PageIdentity $page)
Factory function.
Definition: Category.php:159
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
Page\ParserOutputAccess
Service for getting rendered output of a given page.
Definition: ParserOutputAccess.php:48
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:89
WikiPage\hasViewableContent
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition: WikiPage.php:614
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:721
WikiPage\__toString
__toString()
Returns an informative human readable unique representation of the page identity, for use as a cache ...
Definition: WikiPage.php:3376
WikiPage\prepareContentForEdit
prepareContentForEdit(Content $content, RevisionRecord $revision=null, UserIdentity $user=null, $serialFormat=null, $useStash=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:2042
WikiPage\getUserText
getUserText( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:919
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1624
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1670
WikiPage\getLanguage
getLanguage()
Definition: WikiPage.php:731
WikiPage\$mDataLoadedFrom
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:137
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:63
WikiPage\replaceSectionContent
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1585
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2011
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1169
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1082
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:595
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1167
WikiPage\getContributors
getContributors()
Get a list of users who have edited this article, not including the user who made the most recent rev...
Definition: WikiPage.php:1210
MediaWiki\User\UserIdentity\getId
getId( $wikiId=self::LOCAL)
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
WikiPage\doEditUpdates
doEditUpdates(RevisionRecord $revisionRecord, UserIdentity $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2104
WikiPage\doViewUpdates
doViewUpdates(Authority $performer, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1314
$success
$success
Definition: NoLocalSettings.php:42
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
ParserOutput\hasReducedExpiry
hasReducedExpiry()
Check whether the cache TTL was lowered from the site default.
Definition: ParserOutput.php:1697
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Definition: FakeResultWrapper.php:12
WikiPage\getDBLoadBalancer
getDBLoadBalancer()
Definition: WikiPage.php:282
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:292
WikiPage\insertRedirectEntry
insertRedirectEntry(LinkTarget $rt, $oldLatest=null)
Insert or update the redirect table entry for this page to indicate it redirects to $rt.
Definition: WikiPage.php:1112
WikiPage\$mLanguage
string null $mLanguage
Definition: WikiPage.php:157
RefreshLinksJob\newPrioritized
static newPrioritized(PageIdentity $page, array $params)
Definition: RefreshLinksJob.php:71
Page\PageReference
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Definition: PageReference.php:49
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:76
WikiPage\$mTitle
Title $mTitle
Definition: WikiPage.php:75
WikiPage\$mTouched
string $mTouched
Definition: WikiPage.php:152
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
WikiPage\triggerOpportunisticLinksUpdate
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs after a fresh parser output was generated.
Definition: WikiPage.php:3174
Page\PageStoreRecord
Immutable data record representing an editable page on a wiki.
Definition: PageStoreRecord.php:33
WikiPage\protectDescription
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2588
Title\castFromPageIdentity
static castFromPageIdentity(?PageIdentity $pageIdentity)
Return a Title for a given PageIdentity.
Definition: Title.php:326
$dbr
$dbr
Definition: testCompression.php:54
WikiPage\updateParserCache
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2134
MediaWiki\Linker\LinkTarget\getNamespace
getNamespace()
Get the namespace index.
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1567
WikiPage\doDeleteArticleBatched
doDeleteArticleBatched( $reason, $suppress, UserIdentity $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2746
MWException
MediaWiki exception.
Definition: MWException.php:29
WikiPage\updateRevisionOn
updateRevisionOn( $dbw, RevisionRecord $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1422
WikiPage\isSamePageAs
isSamePageAs(PageReference $other)
Checks whether the given PageReference refers to the same page as this PageReference....
Definition: WikiPage.php:3387
WikiPage\getDBkey
getDBkey()
Get the page title in DB key form.This should always return a valid DB key.string
Definition: WikiPage.php:3352
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:955
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
WikiPage\hasDifferencesOutsideMainSlot
static hasDifferencesOutsideMainSlot(RevisionRecord $a, RevisionRecord $b)
Helper method for checking whether two revisions have differences that go beyond the main slot.
Definition: WikiPage.php:1549
WikiPage\onArticleEdit
static onArticleEdit(Title $title, RevisionRecord $revRecord=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:2936
Page\PageReference\getNamespace
getNamespace()
Returns the page's namespace number.
WikiPage\doSecondaryDataUpdates
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2178
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2186
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:357
MediaWiki\Storage\PreparedUpdate
An object representing a page update during an edit.
Definition: PreparedUpdate.php:22
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1377
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1261
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:314
WikiPage\exists
exists()
Definition: WikiPage.php:599
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:188
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:2880
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:125
WikiPage\$mRedirectTarget
Title $mRedirectTarget
The cache of the redirect target.
Definition: WikiPage.php:103
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:713
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:743
WikiPage\purgeInterwikiCheckKey
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:2984
WikiPage\$mDataLoaded
bool $mDataLoaded
Definition: WikiPage.php:82
ParserOptions\newCanonical
static newCanonical( $context, $userLang=null)
Creates a "canonical" ParserOptions object.
Definition: ParserOptions.php:1089
MediaWiki\User\UserIdentity\getName
getName()
$title
$title
Definition: testCompression.php:38
WikiPage\doDeleteArticleReal
doDeleteArticleReal( $reason, UserIdentity $deleter, $suppress=false, $u1=null, &$error='', $u2=null, $tags=[], $logsubtype='delete', $immediate=false)
Back-end article deletion Deletes the article with database consistency, writes logs,...
Definition: WikiPage.php:2699
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:648
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
WikiPage\setTimestamp
setTimestamp( $ts)
Set the page timestamp (use only to avoid DB queries)
Definition: WikiPage.php:865
MediaWiki\Permissions\Authority\authorizeWrite
authorizeWrite(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize write access.
$revStore
$revStore
Definition: testCompression.php:55
WikiPage\getDerivedDataUpdater
getDerivedDataUpdater(UserIdentity $forUser=null, RevisionRecord $forRevision=null, RevisionSlotsUpdate $forUpdate=null, $forEdit=false)
Returns a DerivedPageDataUpdater for use with the given target revision or new content.
Definition: WikiPage.php:1709
IDBAccessObject\READ_NONE
const READ_NONE
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:75
WikiPage\$mLatest
int false $mLatest
False means "not loaded".
Definition: WikiPage.php:120
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
WikiPage\doPurge
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1342
WikiPage\isCountable
isCountable( $editInfo=false)
Determine whether a page would be suitable for being counted as an article in the site_stats table ba...
Definition: WikiPage.php:974
Page\PageReference\getWikiId
getWikiId()
Get the ID of the wiki this page belongs to.
WikiPage\getContentModel
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:674
WikiPage\$mPageIsRedirectField
bool $mPageIsRedirectField
A cache of the page_is_redirect field, loaded with page data.
Definition: WikiPage.php:88
MediaWiki\Storage\EditResult
Object for storing information about the effects of an edit.
Definition: EditResult.php:38
WikiPage\pageDataFromTitle
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object's namespace and title using a sanitized title string.
Definition: WikiPage.php:435
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2779
MediaWiki\Revision\RevisionRecord\getSlots
getSlots()
Returns the slots defined for this revision.
Definition: RevisionRecord.php:222
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
MediaWiki\Storage\RevisionSlotsUpdate
Value object representing a modification of revision slots.
Definition: RevisionSlotsUpdate.php:36
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:674
Page\ExistingPageRecord
Data record representing a page that currently exists as an editable page on a wiki.
Definition: ExistingPageRecord.php:15
WikiPage\$mHasRedirectTarget
bool null $mHasRedirectTarget
Boolean if the redirect status is definitively known.
Definition: WikiPage.php:96
WikiPage\getId
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:587
$content
$content
Definition: router.php:76
WikiPage\getDeletionUpdates
getDeletionUpdates( $rev=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3242
MediaWiki\DAO\WikiAwareEntity\assertWiki
assertWiki( $wikiId)
Throws if $wikiId is different from the return value of getWikiId().
WikiPage\protectDescriptionLog
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2630
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:52
WikiPage\insertRedirect
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1086
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition: WikiPage.php:840
Page\PageReference\getDBkey
getDBkey()
Get the page title in DB key form.
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
MediaWiki\Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:44
DB_PRIMARY
const DB_PRIMARY
Definition: defines.php:27
MediaWiki\Storage\PageUpdater
Controller-like object for creating and updating pages by creating new revisions.
Definition: PageUpdater.php:81
HTMLCacheUpdateJob\newForBacklinks
static newForBacklinks(PageReference $page, $table, $params=[])
Definition: HTMLCacheUpdateJob.php:61
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:131
WikiPage\newFromID
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:218
WikiPage\isNew
isNew()
Tests if the page is new (only has one revision).
Definition: WikiPage.php:656
WikiPage\getSourceURL
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3310
MediaWiki\Linker\LinkTarget\getDBkey
getDBkey()
Get the main part with underscores.
MediaWiki\Linker\LinkTarget\getFragment
getFragment()
Get the link fragment (i.e.
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1440
WikiPage\$derivedDataUpdater
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:167
WikiPage\doEditContent
doEditContent(Content $content, $summary, $flags=0, $originalRevId=false, Authority $performer=null, $serialFormat=null, $tags=[], $undidRevId=0)
Change an existing article or create a new article.
Definition: WikiPage.php:1851
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3034
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:234
WikiPage\$mLastRevision
RevisionRecord null $mLastRevision
Definition: WikiPage.php:142
WikiPage\canExist
canExist()
Definition: WikiPage.php:3368
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:94
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:484
WikiPage\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object.
Definition: WikiPage.php:370
WikiPage\loadLastEdit
loadLastEdit()
Loads everything except the text This isn't necessary for all uses, so it's only done if needed.
Definition: WikiPage.php:768
WikiPage\setLastEdit
setLastEdit(RevisionRecord $revRecord)
Set the latest revision.
Definition: WikiPage.php:807
WikiPage\insertNullProtectionRevision
insertNullProtectionRevision(string $revCommentMsg, array $limit, array $expiry, bool $cascade, string $reason, UserIdentity $user)
Insert a new null revision for this page.
Definition: WikiPage.php:2506
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1158
WikiPage\getPageUpdaterFactory
getPageUpdaterFactory()
Definition: WikiPage.php:261
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:126
Content
Base interface for content objects.
Definition: Content.php:35
WikiPage\loadFromRow
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:540
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:6207
RefreshLinksJob\newDynamic
static newDynamic(PageIdentity $page, array $params)
Definition: RefreshLinksJob.php:83
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2566
Title
Represents a title within MediaWiki.
Definition: Title.php:47
wfRandom
wfRandom()
Get a random decimal value in the domain of [0, 1), in a way not likely to give duplicate values for ...
Definition: GlobalFunctions.php:239
MediaWiki\Permissions\Authority\isAllowed
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
WikiPage\$mIsRedirect
bool $mIsRedirect
Definition: WikiPage.php:113
InfoAction\invalidateCache
static invalidateCache(PageIdentity $page, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:181
JobQueueGroup\singleton
static singleton( $domain=false)
Definition: JobQueueGroup.php:114
wfReadOnlyReason
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
Definition: GlobalFunctions.php:1097
WikiPage\getUser
getUser( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:879
$cache
$cache
Definition: mcc.php:33
MediaWiki\Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:279
WikiPage\factory
static factory(PageIdentity $pageIdentity)
Create a WikiPage object of the appropriate class for the given PageIdentity.
Definition: WikiPage.php:203
WikiPage\doDeleteUpdates
doDeleteUpdates( $id, Content $content=null, RevisionRecord $revRecord=null, UserIdentity $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:2809
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:49
WikiPage\$mId
int $mId
Definition: WikiPage.php:132
WikiPage\getWikiDisplayName
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3297
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:244
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
WikiPage\updateCategoryCounts
updateCategoryCounts(array $added, array $deleted, $id=0)
Update all the appropriate counts in the category table, given that we've added the categories $added...
Definition: WikiPage.php:3083
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3319
MediaWiki\Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
Definition: RevisionRecord.php:459
WikiPage\getLatest
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition: WikiPage.php:755
MediaWiki\Storage\PageUpdaterFactory
A factory for PageUpdater instances.
Definition: PageUpdaterFactory.php:56
$source
$source
Definition: mwdoc-filter.php:34
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:45
WikiPage\pageData
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:406
Page\DeletePage
Definition: DeletePage.php:49
Page\DeletePage\PAGE_BASE
const PAGE_BASE
Constants used for the return value of getSuccessfulDeletionsIDs() and deletionsWereScheduled()
Definition: DeletePage.php:62
MediaWiki\Edit\PreparedEdit
Represents information returned by WikiPage::prepareContentForEdit()
Definition: PreparedEdit.php:35
WikiPage\isLocal
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3284
Category\newFromName
static newFromName( $name)
Factory function.
Definition: Category.php:139
WikiPage\getCurrentUpdate
getCurrentUpdate()
Get the state of an ongoing update, shortly before or just after it is saved to the database.
Definition: WikiPage.php:2484
Title\castFromLinkTarget
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:313
WikiPage\newPageUpdater
newPageUpdater( $performer, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
Definition: WikiPage.php:1771
NS_FILE
const NS_FILE
Definition: Defines.php:70
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:851
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1510
WikiPage\$mPreparedEdit
PreparedEdit false $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:127
WikiPage\$mLinksUpdated
string $mLinksUpdated
Definition: WikiPage.php:162
WikiPage\isRedirect
isRedirect()
Is the page a redirect, according to secondary tracking tables? If this is true, getRedirectTarget() ...
Definition: WikiPage.php:624
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:127
WikiPage\__construct
__construct(PageIdentity $pageIdentity)
Definition: WikiPage.php:172
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:106
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:66
CommentStore\getStore
static getStore()
Definition: CommentStore.php:120
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
Definition: DeferredUpdates.php:151
WikiPage\getCreator
getCreator( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:900
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:453
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:305
WikiPage\__wakeup
__wakeup()
Ensure consistency when unserializing.
Definition: WikiPage.php:3331
CommentStoreComment
Value object for a comment stored by CommentStore.
Definition: CommentStoreComment.php:30
WikiPage\$mTimestamp
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition: WikiPage.php:147
WikiPage\$mIsNew
bool $mIsNew
Definition: WikiPage.php:108
Title\purgeExpiredRestrictions
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:2694
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
MediaWiki\Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:180
WikiPage\getNamespace
getNamespace()
Returns the page's namespace number.The value returned by this method should represent a valid namesp...
Definition: WikiPage.php:3344
WikiPage\getParserOutput
getParserOutput(?ParserOptions $parserOptions=null, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1284
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
WikiPage\doUpdateRestrictions
doUpdateRestrictions(array $limit, array $expiry, &$cascade, $reason, UserIdentity $user, $tags=null)
Update the article's restriction field, and leave a log entry.
Definition: WikiPage.php:2208
WikiPage\getContentHandlerFactory
getContentHandlerFactory()
Definition: WikiPage.php:275
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:322
WikiPage\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory=false)
Auto-generates a deletion reason.
Definition: WikiPage.php:3065
WikiPage\getRevisionStore
getRevisionStore()
Definition: WikiPage.php:268
$type
$type
Definition: testCompression.php:52