MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
26 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
47 use Wikimedia\Assert\Assert;
48 use Wikimedia\Assert\PreconditionException;
49 use Wikimedia\NonSerializable\NonSerializableTrait;
53 
61  use NonSerializableTrait;
62  use ProtectedHookAccessorTrait;
64 
65  // Constants for $mDataLoadedFrom and related
66 
72  public $mTitle;
73 
79  public $mDataLoaded = false;
80 
85  private $mPageIsRedirectField = false;
86 
93  private $mHasRedirectTarget = null;
94 
100  protected $mRedirectTarget = null;
101 
105  private $mIsNew = false;
106 
110  private $mIsRedirect = false;
111 
117  public $mLatest = false;
118 
124  public $mPreparedEdit = false;
125 
129  protected $mId = null;
130 
135 
139  private $mLastRevision = null;
140 
144  protected $mTimestamp = '';
145 
149  protected $mTouched = '19700101000000';
150 
154  protected $mLanguage = null;
155 
159  protected $mLinksUpdated = '19700101000000';
160 
164  private $derivedDataUpdater = null;
165 
169  public function __construct( PageIdentity $pageIdentity ) {
170  $pageIdentity->assertWiki( PageIdentity::LOCAL );
171 
172  // TODO: remove the need for casting to Title.
173  $title = Title::castFromPageIdentity( $pageIdentity );
174  if ( !$title->canExist() ) {
175  throw new InvalidArgumentException( "WikiPage constructed on a Title that cannot exist as a page: $title" );
176  }
177 
178  $this->mTitle = $title;
179  }
180 
185  public function __clone() {
186  $this->mTitle = clone $this->mTitle;
187  }
188 
200  public static function factory( PageIdentity $pageIdentity ) {
201  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $pageIdentity );
202  }
203 
215  public static function newFromID( $id, $from = 'fromdb' ) {
216  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromID( $id, $from );
217  }
218 
231  public static function newFromRow( $row, $from = 'fromdb' ) {
232  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromRow( $row, $from );
233  }
234 
241  public static function convertSelectType( $type ) {
242  switch ( $type ) {
243  case 'fromdb':
244  return self::READ_NORMAL;
245  case 'fromdbmaster':
246  return self::READ_LATEST;
247  case 'forupdate':
248  return self::READ_LOCKING;
249  default:
250  // It may already be an integer or whatever else
251  return $type;
252  }
253  }
254 
259  return MediaWikiServices::getInstance()->getPageUpdaterFactory();
260  }
261 
265  private function getRevisionStore() {
266  return MediaWikiServices::getInstance()->getRevisionStore();
267  }
268 
273  return MediaWikiServices::getInstance()->getContentHandlerFactory();
274  }
275 
279  private function getDBLoadBalancer() {
280  return MediaWikiServices::getInstance()->getDBLoadBalancer();
281  }
282 
289  public function getActionOverrides() {
290  return $this->getContentHandler()->getActionOverrides();
291  }
292 
302  public function getContentHandler() {
303  return $this->getContentHandlerFactory()
304  ->getContentHandler( $this->getContentModel() );
305  }
306 
311  public function getTitle(): Title {
312  return $this->mTitle;
313  }
314 
319  public function clear() {
320  $this->mDataLoaded = false;
321  $this->mDataLoadedFrom = self::READ_NONE;
322 
323  $this->clearCacheFields();
324  }
325 
330  protected function clearCacheFields() {
331  $this->mId = null;
332  $this->mRedirectTarget = null; // Title object if set
333  $this->mHasRedirectTarget = null;
334  $this->mPageIsRedirectField = false;
335  $this->mLastRevision = null; // Latest revision
336  $this->mTouched = '19700101000000';
337  $this->mLanguage = null;
338  $this->mLinksUpdated = '19700101000000';
339  $this->mTimestamp = '';
340  $this->mIsNew = false;
341  $this->mIsRedirect = false;
342  $this->mLatest = false;
343  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
344  // checks the requested rev ID and content against the cached one. For most
345  // content types, the output should not change during the lifetime of this cache.
346  // Clearing it can cause extra parses on edit for no reason.
347  }
348 
354  public function clearPreparedEdit() {
355  $this->mPreparedEdit = false;
356  }
357 
367  public static function getQueryInfo() {
368  global $wgPageLanguageUseDB;
369 
370  $ret = [
371  'tables' => [ 'page' ],
372  'fields' => [
373  'page_id',
374  'page_namespace',
375  'page_title',
376  'page_restrictions',
377  'page_is_redirect',
378  'page_is_new',
379  'page_random',
380  'page_touched',
381  'page_links_updated',
382  'page_latest',
383  'page_len',
384  'page_content_model',
385  ],
386  'joins' => [],
387  ];
388 
389  if ( $wgPageLanguageUseDB ) {
390  $ret['fields'][] = 'page_lang';
391  }
392 
393  return $ret;
394  }
395 
403  protected function pageData( $dbr, $conditions, $options = [] ) {
404  $pageQuery = self::getQueryInfo();
405 
406  $this->getHookRunner()->onArticlePageDataBefore(
407  $this, $pageQuery['fields'], $pageQuery['tables'], $pageQuery['joins'] );
408 
409  $row = $dbr->selectRow(
410  $pageQuery['tables'],
411  $pageQuery['fields'],
412  $conditions,
413  __METHOD__,
414  $options,
415  $pageQuery['joins']
416  );
417 
418  $this->getHookRunner()->onArticlePageDataAfter( $this, $row );
419 
420  return $row;
421  }
422 
432  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
433  if ( !$title->canExist() ) {
434  return false;
435  }
436 
437  return $this->pageData( $dbr, [
438  'page_namespace' => $title->getNamespace(),
439  'page_title' => $title->getDBkey() ], $options );
440  }
441 
450  public function pageDataFromId( $dbr, $id, $options = [] ) {
451  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
452  }
453 
466  public function loadPageData( $from = 'fromdb' ) {
467  $from = self::convertSelectType( $from );
468  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
469  // We already have the data from the correct location, no need to load it twice.
470  return;
471  }
472 
473  if ( is_int( $from ) ) {
474  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
475  $loadBalancer = $this->getDBLoadBalancer();
476  $db = $loadBalancer->getConnection( $index );
477  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
478 
479  if ( !$data
480  && $index == DB_REPLICA
481  && $loadBalancer->getServerCount() > 1
482  && $loadBalancer->hasOrMadeRecentPrimaryChanges()
483  ) {
484  $from = self::READ_LATEST;
485  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
486  $db = $loadBalancer->getConnection( $index );
487  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
488  }
489  } else {
490  // No idea from where the caller got this data, assume replica DB.
491  $data = $from;
492  $from = self::READ_NORMAL;
493  }
494 
495  $this->loadFromRow( $data, $from );
496  }
497 
511  public function wasLoadedFrom( $from ) {
512  $from = self::convertSelectType( $from );
513 
514  if ( !is_int( $from ) ) {
515  // No idea from where the caller got this data, assume replica DB.
516  $from = self::READ_NORMAL;
517  }
518 
519  if ( $from <= $this->mDataLoadedFrom ) {
520  return true;
521  }
522 
523  return false;
524  }
525 
537  public function loadFromRow( $data, $from ) {
538  $lc = MediaWikiServices::getInstance()->getLinkCache();
539  $lc->clearLink( $this->mTitle );
540 
541  if ( $data ) {
542  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
543 
544  $this->mTitle->loadFromRow( $data );
545 
546  // Old-fashioned restrictions
547  $this->mTitle->loadRestrictions( $data->page_restrictions );
548 
549  $this->mId = intval( $data->page_id );
550  $this->mTouched = MWTimestamp::convert( TS_MW, $data->page_touched );
551  $this->mLanguage = $data->page_lang ?? null;
552  $this->mLinksUpdated = $data->page_links_updated === null
553  ? null
554  : MWTimestamp::convert( TS_MW, $data->page_links_updated );
555  $this->mPageIsRedirectField = (bool)$data->page_is_redirect;
556  $this->mIsNew = intval( $data->page_is_new ?? 0 );
557  $this->mIsRedirect = intval( $data->page_is_redirect ?? 0 );
558  $this->mLatest = intval( $data->page_latest );
559  // T39225: $latest may no longer match the cached latest RevisionRecord object.
560  // Double-check the ID of any cached latest RevisionRecord object for consistency.
561  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
562  $this->mLastRevision = null;
563  $this->mTimestamp = '';
564  }
565  } else {
566  $lc->addBadLinkObj( $this->mTitle );
567 
568  $this->mTitle->loadFromRow( false );
569 
570  $this->clearCacheFields();
571 
572  $this->mId = 0;
573  }
574 
575  $this->mDataLoaded = true;
576  $this->mDataLoadedFrom = self::convertSelectType( $from );
577  }
578 
584  public function getId( $wikiId = self::LOCAL ): int {
585  $this->assertWiki( $wikiId );
586 
587  if ( !$this->mDataLoaded ) {
588  $this->loadPageData();
589  }
590  return $this->mId;
591  }
592 
596  public function exists(): bool {
597  if ( !$this->mDataLoaded ) {
598  $this->loadPageData();
599  }
600  return $this->mId > 0;
601  }
602 
611  public function hasViewableContent() {
612  return $this->mTitle->isKnown();
613  }
614 
621  public function isRedirect() {
622  if ( !$this->mDataLoaded ) {
623  $this->loadPageData();
624  }
625 
626  return (bool)$this->mIsRedirect;
627  }
628 
638  public function getPageIsRedirectField() {
639  if ( !$this->mDataLoaded ) {
640  $this->loadPageData();
641  }
643  }
644 
653  public function isNew() {
654  if ( !$this->mDataLoaded ) {
655  $this->loadPageData();
656  }
657 
658  return (bool)$this->mIsNew;
659  }
660 
671  public function getContentModel() {
672  if ( $this->exists() ) {
673  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
674 
675  return $cache->getWithSetCallback(
676  $cache->makeKey( 'page-content-model', $this->getLatest() ),
677  $cache::TTL_MONTH,
678  function () {
679  $rev = $this->getRevisionRecord();
680  if ( $rev ) {
681  // Look at the revision's actual content model
682  $slot = $rev->getSlot(
683  SlotRecord::MAIN,
684  RevisionRecord::RAW
685  );
686  return $slot->getModel();
687  } else {
688  LoggerFactory::getInstance( 'wikipage' )->warning(
689  'Page exists but has no (visible) revisions!',
690  [
691  'page-title' => $this->mTitle->getPrefixedDBkey(),
692  'page-id' => $this->getId(),
693  ]
694  );
695  return $this->mTitle->getContentModel();
696  }
697  },
698  [ 'pcTTL' => $cache::TTL_PROC_LONG ]
699  );
700  }
701 
702  // use the default model for this page
703  return $this->mTitle->getContentModel();
704  }
705 
710  public function checkTouched() {
711  return ( $this->exists() && !$this->isRedirect() );
712  }
713 
718  public function getTouched() {
719  if ( !$this->mDataLoaded ) {
720  $this->loadPageData();
721  }
722  return $this->mTouched;
723  }
724 
728  public function getLanguage() {
729  if ( !$this->mDataLoaded ) {
730  $this->loadLastEdit();
731  }
732 
733  return $this->mLanguage;
734  }
735 
740  public function getLinksTimestamp() {
741  if ( !$this->mDataLoaded ) {
742  $this->loadPageData();
743  }
744  return $this->mLinksUpdated;
745  }
746 
752  public function getLatest( $wikiId = self::LOCAL ) {
753  $this->assertWiki( $wikiId );
754 
755  if ( !$this->mDataLoaded ) {
756  $this->loadPageData();
757  }
758  return (int)$this->mLatest;
759  }
760 
765  protected function loadLastEdit() {
766  if ( $this->mLastRevision !== null ) {
767  return; // already loaded
768  }
769 
770  $latest = $this->getLatest();
771  if ( !$latest ) {
772  return; // page doesn't exist or is missing page_latest info
773  }
774 
775  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
776  // T39225: if session S1 loads the page row FOR UPDATE, the result always
777  // includes the latest changes committed. This is true even within REPEATABLE-READ
778  // transactions, where S1 normally only sees changes committed before the first S1
779  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
780  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
781  // happened after the first S1 SELECT.
782  // https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html#isolevel_repeatable-read
783  $revision = $this->getRevisionStore()
784  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LOCKING );
785  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
786  // Bug T93976: if page_latest was loaded from the primary DB, fetch the
787  // revision from there as well, as it may not exist yet on a replica DB.
788  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
789  $revision = $this->getRevisionStore()
790  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LATEST );
791  } else {
792  $revision = $this->getRevisionStore()->getKnownCurrentRevision( $this->getTitle(), $latest );
793  }
794 
795  if ( $revision ) { // sanity
796  $this->setLastEdit( $revision );
797  }
798  }
799 
804  private function setLastEdit( RevisionRecord $revRecord ) {
805  $this->mLastRevision = $revRecord;
806  $this->mLatest = $revRecord->getId();
807  $this->mTimestamp = $revRecord->getTimestamp();
808  $this->mTouched = max( $this->mTouched, $revRecord->getTimestamp() );
809  }
810 
816  public function getRevisionRecord() {
817  $this->loadLastEdit();
818  if ( $this->mLastRevision ) {
819  return $this->mLastRevision;
820  }
821  return null;
822  }
823 
837  public function getContent( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
838  $this->loadLastEdit();
839  if ( $this->mLastRevision ) {
840  return $this->mLastRevision->getContent( SlotRecord::MAIN, $audience, $performer );
841  }
842  return null;
843  }
844 
848  public function getTimestamp() {
849  // Check if the field has been filled by WikiPage::setTimestamp()
850  if ( !$this->mTimestamp ) {
851  $this->loadLastEdit();
852  }
853 
854  return MWTimestamp::convert( TS_MW, $this->mTimestamp );
855  }
856 
862  public function setTimestamp( $ts ) {
863  $this->mTimestamp = MWTimestamp::convert( TS_MW, $ts );
864  }
865 
876  public function getUser( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
877  $this->loadLastEdit();
878  if ( $this->mLastRevision ) {
879  $revUser = $this->mLastRevision->getUser( $audience, $performer );
880  return $revUser ? $revUser->getId() : 0;
881  } else {
882  return -1;
883  }
884  }
885 
897  public function getCreator( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
898  $revRecord = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
899  if ( $revRecord ) {
900  return $revRecord->getUser( $audience, $performer );
901  } else {
902  return null;
903  }
904  }
905 
916  public function getUserText( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
917  $this->loadLastEdit();
918  if ( $this->mLastRevision ) {
919  $revUser = $this->mLastRevision->getUser( $audience, $performer );
920  return $revUser ? $revUser->getName() : '';
921  } else {
922  return '';
923  }
924  }
925 
937  public function getComment( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
938  $this->loadLastEdit();
939  if ( $this->mLastRevision ) {
940  $revComment = $this->mLastRevision->getComment( $audience, $performer );
941  return $revComment ? $revComment->text : '';
942  } else {
943  return '';
944  }
945  }
946 
952  public function getMinorEdit() {
953  $this->loadLastEdit();
954  if ( $this->mLastRevision ) {
955  return $this->mLastRevision->isMinor();
956  } else {
957  return false;
958  }
959  }
960 
969  public function isCountable( $editInfo = false ) {
970  global $wgArticleCountMethod;
971 
972  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
973 
974  if ( !$this->mTitle->isContentPage() ) {
975  return false;
976  }
977 
978  if ( $editInfo ) {
979  // NOTE: only the main slot can make a page a redirect
980  $content = $editInfo->pstContent;
981  } else {
982  $content = $this->getContent();
983  }
984 
985  if ( !$content || $content->isRedirect() ) {
986  return false;
987  }
988 
989  $hasLinks = null;
990 
991  if ( $wgArticleCountMethod === 'link' ) {
992  // nasty special case to avoid re-parsing to detect links
993 
994  if ( $editInfo ) {
995  // ParserOutput::getLinks() is a 2D array of page links, so
996  // to be really correct we would need to recurse in the array
997  // but the main array should only have items in it if there are
998  // links.
999  $hasLinks = (bool)count( $editInfo->output->getLinks() );
1000  } else {
1001  // NOTE: keep in sync with RevisionRenderer::getLinkCount
1002  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
1003  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', '1',
1004  [ 'pl_from' => $this->getId() ], __METHOD__ );
1005  }
1006  }
1007 
1008  // TODO: MCR: determine $hasLinks for each slot, and use that info
1009  // with that slot's Content's isCountable method. That requires per-
1010  // slot ParserOutput in the ParserCache, or per-slot info in the
1011  // pagelinks table.
1012  return $content->isCountable( $hasLinks );
1013  }
1014 
1023  public function getRedirectTarget() {
1024  if ( $this->mRedirectTarget !== null ) {
1025  return $this->mRedirectTarget;
1026  }
1027 
1028  if ( $this->mHasRedirectTarget === false || !$this->getPageIsRedirectField() ) {
1029  return null;
1030  }
1031 
1032  // Query the redirect table
1033  $dbr = wfGetDB( DB_REPLICA );
1034  $row = $dbr->selectRow( 'redirect',
1035  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1036  [ 'rd_from' => $this->getId() ],
1037  __METHOD__
1038  );
1039 
1040  // rd_fragment and rd_interwiki were added later, populate them if empty
1041  if ( $row && $row->rd_fragment !== null && $row->rd_interwiki !== null ) {
1042  // (T203942) We can't redirect to Media namespace because it's virtual.
1043  // We don't want to modify Title objects farther down the
1044  // line. So, let's fix this here by changing to File namespace.
1045  if ( $row->rd_namespace == NS_MEDIA ) {
1046  $namespace = NS_FILE;
1047  } else {
1048  $namespace = $row->rd_namespace;
1049  }
1050  // T261347: be defensive when fetching data from the redirect table.
1051  // Use Title::makeTitleSafe(), and if that returns null, ignore the
1052  // row. In an ideal world, the DB would be cleaned up after a
1053  // namespace change, but nobody could be bothered to do that.
1054  $this->mRedirectTarget = Title::makeTitleSafe(
1055  $namespace, $row->rd_title,
1056  $row->rd_fragment, $row->rd_interwiki
1057  );
1058  $this->mHasRedirectTarget = $this->mRedirectTarget !== null;
1059  return $this->mRedirectTarget;
1060  }
1061 
1062  // This page doesn't have an entry in the redirect table
1063  $this->mRedirectTarget = $this->insertRedirect();
1064  $this->mHasRedirectTarget = $this->mRedirectTarget !== null;
1065  return $this->mRedirectTarget;
1066  }
1067 
1076  public function insertRedirect() {
1077  $content = $this->getContent();
1078  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1079  if ( !$retval ) {
1080  return null;
1081  }
1082 
1083  // Update the DB post-send if the page has not cached since now
1084  $latest = $this->getLatest();
1086  function () use ( $retval, $latest ) {
1087  $this->insertRedirectEntry( $retval, $latest );
1088  },
1089  DeferredUpdates::POSTSEND,
1090  wfGetDB( DB_PRIMARY )
1091  );
1092 
1093  return $retval;
1094  }
1095 
1102  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1103  if ( !$rt->isValidRedirectTarget() ) {
1104  // Don't put a bad redirect into the database (T278367)
1105  return false;
1106  }
1107 
1108  $dbw = wfGetDB( DB_PRIMARY );
1109  $dbw->startAtomic( __METHOD__ );
1110 
1111  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1112  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1113  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1114  $dbw->upsert(
1115  'redirect',
1116  [
1117  'rd_from' => $this->getId(),
1118  'rd_namespace' => $rt->getNamespace(),
1119  'rd_title' => $rt->getDBkey(),
1120  'rd_fragment' => $truncatedFragment,
1121  'rd_interwiki' => $rt->getInterwiki(),
1122  ],
1123  'rd_from',
1124  [
1125  'rd_namespace' => $rt->getNamespace(),
1126  'rd_title' => $rt->getDBkey(),
1127  'rd_fragment' => $truncatedFragment,
1128  'rd_interwiki' => $rt->getInterwiki(),
1129  ],
1130  __METHOD__
1131  );
1132  $success = true;
1133  } else {
1134  $success = false;
1135  }
1136 
1137  $dbw->endAtomic( __METHOD__ );
1138 
1139  return $success;
1140  }
1141 
1147  public function followRedirect() {
1148  return $this->getRedirectURL( $this->getRedirectTarget() );
1149  }
1150 
1158  public function getRedirectURL( $rt ) {
1159  if ( !$rt ) {
1160  return false;
1161  }
1162 
1163  if ( $rt->isExternal() ) {
1164  if ( $rt->isLocal() ) {
1165  // Offsite wikis need an HTTP redirect.
1166  // This can be hard to reverse and may produce loops,
1167  // so they may be disabled in the site configuration.
1168  $source = $this->mTitle->getFullURL( 'redirect=no' );
1169  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1170  } else {
1171  // External pages without "local" bit set are not valid
1172  // redirect targets
1173  return false;
1174  }
1175  }
1176 
1177  if ( $rt->isSpecialPage() ) {
1178  // Gotta handle redirects to special pages differently:
1179  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1180  // Some pages are not valid targets.
1181  if ( $rt->isValidRedirectTarget() ) {
1182  return $rt->getFullURL();
1183  } else {
1184  return false;
1185  }
1186  } elseif ( !$rt->isValidRedirectTarget() ) {
1187  // We somehow got a bad redirect target into the database (T278367)
1188  return false;
1189  }
1190 
1191  return $rt;
1192  }
1193 
1199  public function getContributors() {
1200  // @todo: This is expensive; cache this info somewhere.
1201 
1202  $dbr = wfGetDB( DB_REPLICA );
1203 
1204  $actorMigration = ActorMigration::newMigration();
1205  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1206 
1207  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1208 
1209  $revactor_actor = $actorQuery['fields']['rev_actor'];
1210  $fields = [
1211  'user_id' => $actorQuery['fields']['rev_user'],
1212  'user_name' => $actorQuery['fields']['rev_user_text'],
1213  'actor_id' => "MIN($revactor_actor)",
1214  'user_real_name' => 'MIN(user_real_name)',
1215  'timestamp' => 'MAX(rev_timestamp)',
1216  ];
1217 
1218  $conds = [ 'rev_page' => $this->getId() ];
1219 
1220  // The user who made the top revision gets credited as "this page was last edited by
1221  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1222  $user = $this->getUser()
1223  ? User::newFromId( $this->getUser() )
1224  : User::newFromName( $this->getUserText(), false );
1225  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1226 
1227  // Username hidden?
1228  $conds[] = "{$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER )} = 0";
1229 
1230  $jconds = [
1231  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1232  ] + $actorQuery['joins'];
1233 
1234  $options = [
1235  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1236  'ORDER BY' => 'timestamp DESC',
1237  ];
1238 
1239  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1240  return new UserArrayFromResult( $res );
1241  }
1242 
1250  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1251  // NOTE: Keep in sync with ParserOutputAccess::shouldUseCache().
1252  // TODO: Once ParserOutputAccess is stable, deprecated this method.
1253  return $this->exists()
1254  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1255  && $this->getContentHandler()->isParserCacheSupported();
1256  }
1257 
1273  public function getParserOutput(
1274  ParserOptions $parserOptions, $oldid = null, $noCache = false
1275  ) {
1276  if ( $oldid ) {
1277  $revision = $this->getRevisionStore()->getRevisionByTitle( $this->getTitle(), $oldid );
1278 
1279  if ( !$revision ) {
1280  return false;
1281  }
1282  } else {
1283  $revision = $this->getRevisionRecord();
1284  }
1285 
1286  $options = $noCache ? ParserOutputAccess::OPT_NO_CACHE : 0;
1287 
1288  $status = MediaWikiServices::getInstance()->getParserOutputAccess()->getParserOutput(
1289  $this, $parserOptions, $revision, $options
1290  );
1291  return $status->isOK() ? $status->getValue() : false; // convert null to false
1292  }
1293 
1299  public function doViewUpdates( Authority $performer, $oldid = 0 ) {
1300  if ( wfReadOnly() ) {
1301  return;
1302  }
1303 
1304  // Update newtalk / watchlist notification status;
1305  // Avoid outage if the primary DB is not reachable by using a deferred updated
1307  function () use ( $performer, $oldid ) {
1308  $legacyUser = MediaWikiServices::getInstance()
1309  ->getUserFactory()
1310  ->newFromAuthority( $performer );
1311  $this->getHookRunner()->onPageViewUpdates( $this, $legacyUser );
1312 
1313  MediaWikiServices::getInstance()
1314  ->getWatchlistManager()
1315  ->clearTitleUserNotifications( $performer, $this, $oldid );
1316  },
1317  DeferredUpdates::PRESEND
1318  );
1319  }
1320 
1327  public function doPurge() {
1328  if ( !$this->getHookRunner()->onArticlePurge( $this ) ) {
1329  return false;
1330  }
1331 
1332  $this->mTitle->invalidateCache();
1333 
1334  // Clear file cache and send purge after above page_touched update was committed
1335  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1336  $hcu->purgeTitleUrls( $this->mTitle, $hcu::PURGE_PRESEND );
1337 
1338  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) {
1339  MediaWikiServices::getInstance()->getMessageCache()
1340  ->updateMessageOverride( $this->mTitle, $this->getContent() );
1341  }
1342 
1343  return true;
1344  }
1345 
1362  public function insertOn( $dbw, $pageId = null ) {
1363  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1364  $dbw->insert(
1365  'page',
1366  [
1367  'page_namespace' => $this->mTitle->getNamespace(),
1368  'page_title' => $this->mTitle->getDBkey(),
1369  'page_restrictions' => '',
1370  'page_is_redirect' => 0, // Will set this shortly...
1371  'page_is_new' => 1,
1372  'page_random' => wfRandom(),
1373  'page_touched' => $dbw->timestamp(),
1374  'page_latest' => 0, // Fill this in shortly...
1375  'page_len' => 0, // Fill this in shortly...
1376  ] + $pageIdForInsert,
1377  __METHOD__,
1378  [ 'IGNORE' ]
1379  );
1380 
1381  if ( $dbw->affectedRows() > 0 ) {
1382  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1383  $this->mId = $newid;
1384  $this->mTitle->resetArticleID( $newid );
1385 
1386  return $newid;
1387  } else {
1388  return false; // nothing changed
1389  }
1390  }
1391 
1407  public function updateRevisionOn(
1408  $dbw,
1409  RevisionRecord $revision,
1410  $lastRevision = null,
1411  $lastRevIsRedirect = null
1412  ) {
1413  // TODO: move into PageUpdater or PageStore
1414  // NOTE: when doing that, make sure cached fields get reset in doUserEditContent,
1415  // and in the compat stub!
1416 
1417  // Assertion to try to catch T92046
1418  if ( (int)$revision->getId() === 0 ) {
1419  throw new InvalidArgumentException(
1420  __METHOD__ . ': revision has ID ' . var_export( $revision->getId(), 1 )
1421  );
1422  }
1423 
1424  $content = $revision->getContent( SlotRecord::MAIN );
1425  $len = $content ? $content->getSize() : 0;
1426  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1427  $isNew = ( $lastRevision === 0 ) ? 1 : 0;
1428  $isRedirect = $rt !== null ? 1 : 0;
1429 
1430  $conditions = [ 'page_id' => $this->getId() ];
1431 
1432  if ( $lastRevision !== null ) {
1433  // An extra check against threads stepping on each other
1434  $conditions['page_latest'] = $lastRevision;
1435  }
1436 
1437  $revId = $revision->getId();
1438  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1439 
1440  $model = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel();
1441 
1442  $row = [ /* SET */
1443  'page_latest' => $revId,
1444  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1445  'page_is_new' => $isNew,
1446  'page_is_redirect' => $isRedirect,
1447  'page_len' => $len,
1448  'page_content_model' => $model,
1449  ];
1450 
1451  $dbw->update( 'page',
1452  $row,
1453  $conditions,
1454  __METHOD__ );
1455 
1456  $result = $dbw->affectedRows() > 0;
1457  if ( $result ) {
1458  $insertedRow = $this->pageData( $dbw, [ 'page_id' => $this->getId() ] );
1459 
1460  if ( !$insertedRow ) {
1461  throw new MWException( 'Failed to load freshly inserted row' );
1462  }
1463 
1464  $this->mTitle->loadFromRow( $insertedRow );
1465  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1466  $this->setLastEdit( $revision );
1467  $this->mRedirectTarget = null;
1468  $this->mHasRedirectTarget = null;
1469  $this->mPageIsRedirectField = (bool)$rt;
1470  $this->mIsNew = (bool)$isNew;
1471  $this->mIsRedirect = (bool)$isRedirect;
1472 
1473  // Update the LinkCache.
1474  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1475  $linkCache->addGoodLinkObjFromRow(
1476  $this->mTitle,
1477  $insertedRow
1478  );
1479  }
1480 
1481  return $result;
1482  }
1483 
1495  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1496  // Always update redirects (target link might have changed)
1497  // Update/Insert if we don't know if the last revision was a redirect or not
1498  // Delete if changing from redirect to non-redirect
1499  $isRedirect = $redirectTitle !== null;
1500 
1501  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1502  return true;
1503  }
1504 
1505  if ( $isRedirect ) {
1506  $success = $this->insertRedirectEntry( $redirectTitle );
1507  } else {
1508  // This is not a redirect, remove row from redirect table
1509  $where = [ 'rd_from' => $this->getId() ];
1510  $dbw->delete( 'redirect', $where, __METHOD__ );
1511  $success = true;
1512  }
1513 
1514  if ( $this->getTitle()->getNamespace() === NS_FILE ) {
1515  MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
1516  ->invalidateImageRedirect( $this->getTitle() );
1517  }
1518 
1519  return $success;
1520  }
1521 
1535  $aSlots = $a->getSlots();
1536  $bSlots = $b->getSlots();
1537  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1538 
1539  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1540  }
1541 
1552  public function supportsSections() {
1553  return $this->getContentHandler()->supportsSections();
1554  }
1555 
1570  public function replaceSectionContent(
1571  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1572  ) {
1573  $baseRevId = null;
1574  if ( $edittime && $sectionId !== 'new' ) {
1575  $lb = $this->getDBLoadBalancer();
1576  $rev = $this->getRevisionStore()->getRevisionByTimestamp( $this->mTitle, $edittime );
1577  // Try the primary database if this thread may have just added it.
1578  // The logic to fallback to the primary database if the replica is missing
1579  // the revision could be generalized into RevisionStore, but we don't want
1580  // to encourage loading of revisions by timestamp.
1581  if ( !$rev
1582  && $lb->getServerCount() > 1
1583  && $lb->hasOrMadeRecentPrimaryChanges()
1584  ) {
1585  $rev = $this->getRevisionStore()->getRevisionByTimestamp(
1586  $this->mTitle, $edittime, RevisionStore::READ_LATEST );
1587  }
1588  if ( $rev ) {
1589  $baseRevId = $rev->getId();
1590  }
1591  }
1592 
1593  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1594  }
1595 
1609  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1610  $sectionTitle = '', $baseRevId = null
1611  ) {
1612  if ( strval( $sectionId ) === '' ) {
1613  // Whole-page edit; let the whole text through
1614  $newContent = $sectionContent;
1615  } else {
1616  if ( !$this->supportsSections() ) {
1617  throw new MWException( "sections not supported for content model " .
1618  $this->getContentHandler()->getModelID() );
1619  }
1620 
1621  // T32711: always use current version when adding a new section
1622  if ( $baseRevId === null || $sectionId === 'new' ) {
1623  $oldContent = $this->getContent();
1624  } else {
1625  $revRecord = $this->getRevisionStore()->getRevisionById( $baseRevId );
1626  if ( !$revRecord ) {
1627  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1628  $this->getId() . "; section: $sectionId)" );
1629  return null;
1630  }
1631 
1632  $oldContent = $revRecord->getContent( SlotRecord::MAIN );
1633  }
1634 
1635  if ( !$oldContent ) {
1636  wfDebug( __METHOD__ . ": no page text" );
1637  return null;
1638  }
1639 
1640  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1641  }
1642 
1643  return $newContent;
1644  }
1645 
1655  public function checkFlags( $flags ) {
1656  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1657  if ( $this->exists() ) {
1658  $flags |= EDIT_UPDATE;
1659  } else {
1660  $flags |= EDIT_NEW;
1661  }
1662  }
1663 
1664  return $flags;
1665  }
1666 
1694  private function getDerivedDataUpdater(
1695  UserIdentity $forUser = null,
1696  RevisionRecord $forRevision = null,
1697  RevisionSlotsUpdate $forUpdate = null,
1698  $forEdit = false
1699  ) {
1700  if ( !$forRevision && !$forUpdate ) {
1701  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1702  // going to use it with.
1703  $this->derivedDataUpdater = null;
1704  }
1705 
1706  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1707  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1708  // to it did not yet initialize it, because we don't know what data it will be
1709  // initialized with.
1710  $this->derivedDataUpdater = null;
1711  }
1712 
1713  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1714  // However, there is no good way to construct a cache key. We'd need to check against all
1715  // cached instances.
1716 
1717  if ( $this->derivedDataUpdater
1718  && !$this->derivedDataUpdater->isReusableFor(
1719  $forUser,
1720  $forRevision,
1721  $forUpdate,
1722  $forEdit ? $this->getLatest() : null
1723  )
1724  ) {
1725  $this->derivedDataUpdater = null;
1726  }
1727 
1728  if ( !$this->derivedDataUpdater ) {
1729  $this->derivedDataUpdater =
1730  $this->getPageUpdaterFactory()->newDerivedPageDataUpdater( $this );
1731  }
1732 
1734  }
1735 
1756  public function newPageUpdater( $performer, RevisionSlotsUpdate $forUpdate = null ) {
1757  if ( $performer instanceof Authority ) {
1758  // TODO: Deprecate this. But better get rid of this method entirely.
1759  $performer = $performer->getUser();
1760  }
1761 
1762  $pageUpdater = $this->getPageUpdaterFactory()->newPageUpdaterForDerivedPageDataUpdater(
1763  $this,
1764  $performer,
1765  $this->getDerivedDataUpdater( $performer, null, $forUpdate, true )
1766  );
1767 
1768  return $pageUpdater;
1769  }
1770 
1836  public function doEditContent(
1837  Content $content, $summary, $flags = 0, $originalRevId = false,
1838  Authority $performer = null, $serialFormat = null, $tags = [], $undidRevId = 0
1839  ) {
1840  wfDeprecated( __METHOD__, '1.32' );
1841 
1842  if ( !$performer ) {
1843  // Its okay to fallback to $wgUser because this whole method is deprecated
1844  // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser
1845  global $wgUser;
1846  $performer = StubGlobalUser::getRealUser( $wgUser );
1847  }
1848 
1849  return $this->doUserEditContent(
1850  $content, $performer, $summary, $flags, $originalRevId, $tags, $undidRevId
1851  );
1852  }
1853 
1914  public function doUserEditContent(
1915  Content $content,
1916  Authority $performer,
1917  $summary,
1918  $flags = 0,
1919  $originalRevId = false,
1920  $tags = [],
1921  $undidRevId = 0
1922  ) {
1924 
1925  if ( !( $summary instanceof CommentStoreComment ) ) {
1926  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1927  }
1928 
1929  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1930  // Checking the minoredit right should be done in the same place the 'bot' right is
1931  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1932  if ( ( $flags & EDIT_MINOR ) && !$performer->isAllowed( 'minoredit' ) ) {
1933  $flags &= ~EDIT_MINOR;
1934  }
1935 
1936  $slotsUpdate = new RevisionSlotsUpdate();
1937  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1938 
1939  // NOTE: while doUserEditContent() executes, callbacks to getDerivedDataUpdater and
1940  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1941  // used by this PageUpdater. However, there is no guarantee for this.
1942  $updater = $this->newPageUpdater( $performer, $slotsUpdate )
1943  ->setContent( SlotRecord::MAIN, $content )
1944  ->setOriginalRevisionId( $originalRevId );
1945  if ( $undidRevId ) {
1946  $updater->markAsRevert(
1947  EditResult::REVERT_UNDO,
1948  $undidRevId,
1949  $originalRevId ?: null
1950  );
1951  }
1952 
1953  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1954 
1955  // TODO: this logic should not be in the storage layer, it's here for compatibility
1956  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1957  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1958 
1959  if ( $needsPatrol && $performer->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
1960  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1961  }
1962 
1963  $updater->addTags( $tags );
1964 
1965  $revRec = $updater->saveRevision(
1966  $summary,
1967  $flags
1968  );
1969 
1970  // $revRec will be null if the edit failed, or if no new revision was created because
1971  // the content did not change.
1972  if ( $revRec ) {
1973  // update cached fields
1974  // TODO: this is currently redundant to what is done in updateRevisionOn.
1975  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1976  $this->setLastEdit( $revRec );
1977  }
1978 
1979  return $updater->getStatus();
1980  }
1981 
1996  public function makeParserOptions( $context ) {
1997  $options = ParserOptions::newCanonical( $context );
1998 
1999  if ( $this->getTitle()->isConversionTable() ) {
2000  // @todo ConversionTable should become a separate content model, so
2001  // we don't need special cases like this one.
2002  $options->disableContentConversion();
2003  }
2004 
2005  return $options;
2006  }
2007 
2027  public function prepareContentForEdit(
2028  Content $content,
2029  RevisionRecord $revision = null,
2030  UserIdentity $user = null,
2031  $serialFormat = null,
2032  $useCache = true
2033  ) {
2034  if ( !$user ) {
2035  wfDeprecated( __METHOD__ . ' without a UserIdentity', '1.37' );
2036  // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser
2037  global $wgUser;
2038  $user = StubGlobalUser::getRealUser( $wgUser );
2039  }
2040 
2041  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2042  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2043 
2044  if ( !$updater->isUpdatePrepared() ) {
2045  $updater->prepareContent( $user, $slots, $useCache );
2046 
2047  if ( $revision ) {
2048  $updater->prepareUpdate(
2049  $revision,
2050  [
2051  'causeAction' => 'prepare-edit',
2052  'causeAgent' => $user->getName(),
2053  ]
2054  );
2055  }
2056  }
2057 
2058  return $updater->getPreparedEdit();
2059  }
2060 
2089  public function doEditUpdates(
2090  RevisionRecord $revisionRecord,
2091  UserIdentity $user,
2092  array $options = []
2093  ) {
2094  $options += [
2095  'causeAction' => 'edit-page',
2096  'causeAgent' => $user->getName(),
2097  ];
2098 
2099  $updater = $this->getDerivedDataUpdater( $user, $revisionRecord );
2100 
2101  $updater->prepareUpdate( $revisionRecord, $options );
2102 
2103  $updater->doUpdates();
2104  }
2105 
2119  public function updateParserCache( array $options = [] ) {
2120  $revision = $this->getRevisionRecord();
2121  if ( !$revision || !$revision->getId() ) {
2122  LoggerFactory::getInstance( 'wikipage' )->info(
2123  __METHOD__ . ' called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2124  );
2125  return;
2126  }
2127  $userIdentity = $revision->getUser( RevisionRecord::RAW );
2128 
2129  $updater = $this->getDerivedDataUpdater( $userIdentity, $revision );
2130  $updater->prepareUpdate( $revision, $options );
2131  $updater->doParserCacheUpdate();
2132  }
2133 
2163  public function doSecondaryDataUpdates( array $options = [] ) {
2164  $options['recursive'] = $options['recursive'] ?? true;
2165  $revision = $this->getRevisionRecord();
2166  if ( !$revision || !$revision->getId() ) {
2167  LoggerFactory::getInstance( 'wikipage' )->info(
2168  __METHOD__ . ' called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2169  );
2170  return;
2171  }
2172  $userIdentity = $revision->getUser( RevisionRecord::RAW );
2173 
2174  $updater = $this->getDerivedDataUpdater( $userIdentity, $revision );
2175  $updater->prepareUpdate( $revision, $options );
2176  $updater->doSecondaryDataUpdates( $options );
2177  }
2178 
2193  public function doUpdateRestrictions( array $limit, array $expiry,
2194  &$cascade, $reason, UserIdentity $user, $tags = null
2195  ) {
2197 
2198  if ( wfReadOnly() ) {
2199  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2200  }
2201 
2202  $this->loadPageData( 'fromdbmaster' );
2203  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2204  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2205  $id = $this->getId();
2206 
2207  if ( !$cascade ) {
2208  $cascade = false;
2209  }
2210 
2211  // Take this opportunity to purge out expired restrictions
2213 
2214  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2215  // we expect a single selection, but the schema allows otherwise.
2216  $isProtected = false;
2217  $protect = false;
2218  $changed = false;
2219 
2220  $dbw = wfGetDB( DB_PRIMARY );
2221 
2222  foreach ( $restrictionTypes as $action ) {
2223  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2224  $expiry[$action] = 'infinity';
2225  }
2226  if ( !isset( $limit[$action] ) ) {
2227  $limit[$action] = '';
2228  } elseif ( $limit[$action] != '' ) {
2229  $protect = true;
2230  }
2231 
2232  // Get current restrictions on $action
2233  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2234  if ( $current != '' ) {
2235  $isProtected = true;
2236  }
2237 
2238  if ( $limit[$action] != $current ) {
2239  $changed = true;
2240  } elseif ( $limit[$action] != '' ) {
2241  // Only check expiry change if the action is actually being
2242  // protected, since expiry does nothing on an not-protected
2243  // action.
2244  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2245  $changed = true;
2246  }
2247  }
2248  }
2249 
2250  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2251  $changed = true;
2252  }
2253 
2254  // If nothing has changed, do nothing
2255  if ( !$changed ) {
2256  return Status::newGood();
2257  }
2258 
2259  if ( !$protect ) { // No protection at all means unprotection
2260  $revCommentMsg = 'unprotectedarticle-comment';
2261  $logAction = 'unprotect';
2262  } elseif ( $isProtected ) {
2263  $revCommentMsg = 'modifiedarticleprotection-comment';
2264  $logAction = 'modify';
2265  } else {
2266  $revCommentMsg = 'protectedarticle-comment';
2267  $logAction = 'protect';
2268  }
2269 
2270  $logRelationsValues = [];
2271  $logRelationsField = null;
2272  $logParamsDetails = [];
2273 
2274  // Null revision (used for change tag insertion)
2275  $nullRevisionRecord = null;
2276 
2277  if ( $id ) { // Protection of existing page
2278  $legacyUser = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user );
2279  if ( !$this->getHookRunner()->onArticleProtect( $this, $legacyUser, $limit, $reason ) ) {
2280  return Status::newGood();
2281  }
2282 
2283  // Only certain restrictions can cascade...
2284  $editrestriction = isset( $limit['edit'] )
2285  ? [ $limit['edit'] ]
2286  : $this->mTitle->getRestrictions( 'edit' );
2287  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2288  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2289  }
2290  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2291  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2292  }
2293 
2294  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2295  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2296  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2297  }
2298  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2299  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2300  }
2301 
2302  // The schema allows multiple restrictions
2303  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2304  $cascade = false;
2305  }
2306 
2307  // insert null revision to identify the page protection change as edit summary
2308  $latest = $this->getLatest();
2309  $nullRevisionRecord = $this->insertNullProtectionRevision(
2310  $revCommentMsg,
2311  $limit,
2312  $expiry,
2313  $cascade,
2314  $reason,
2315  $user
2316  );
2317 
2318  if ( $nullRevisionRecord === null ) {
2319  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2320  }
2321 
2322  $logRelationsField = 'pr_id';
2323 
2324  // T214035: Avoid deadlock on MySQL.
2325  // Do a DELETE by primary key (pr_id) for any existing protection rows.
2326  // On MySQL and derivatives, unconditionally deleting by page ID (pr_page) would.
2327  // place a gap lock if there are no matching rows. This can deadlock when another
2328  // thread modifies protection settings for page IDs in the same gap.
2329  $existingProtectionIds = $dbw->selectFieldValues(
2330  'page_restrictions',
2331  'pr_id',
2332  [
2333  'pr_page' => $id,
2334  'pr_type' => array_map( 'strval', array_keys( $limit ) )
2335  ],
2336  __METHOD__
2337  );
2338 
2339  if ( $existingProtectionIds ) {
2340  $dbw->delete(
2341  'page_restrictions',
2342  [ 'pr_id' => $existingProtectionIds ],
2343  __METHOD__
2344  );
2345  }
2346 
2347  // Update restrictions table
2348  foreach ( $limit as $action => $restrictions ) {
2349  if ( $restrictions != '' ) {
2350  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2351  $dbw->insert(
2352  'page_restrictions',
2353  [
2354  'pr_page' => $id,
2355  'pr_type' => $action,
2356  'pr_level' => $restrictions,
2357  'pr_cascade' => $cascadeValue,
2358  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2359  ],
2360  __METHOD__
2361  );
2362  $logRelationsValues[] = $dbw->insertId();
2363  $logParamsDetails[] = [
2364  'type' => $action,
2365  'level' => $restrictions,
2366  'expiry' => $expiry[$action],
2367  'cascade' => (bool)$cascadeValue,
2368  ];
2369  }
2370  }
2371 
2372  // Clear out legacy restriction fields
2373  $dbw->update(
2374  'page',
2375  [ 'page_restrictions' => '' ],
2376  [ 'page_id' => $id ],
2377  __METHOD__
2378  );
2379 
2380  $this->getHookRunner()->onRevisionFromEditComplete(
2381  $this, $nullRevisionRecord, $latest, $user, $tags );
2382 
2383  $this->getHookRunner()->onArticleProtectComplete( $this, $legacyUser, $limit, $reason );
2384  } else { // Protection of non-existing page (also known as "title protection")
2385  // Cascade protection is meaningless in this case
2386  $cascade = false;
2387 
2388  if ( $limit['create'] != '' ) {
2389  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2390  $dbw->replace( 'protected_titles',
2391  [ [ 'pt_namespace', 'pt_title' ] ],
2392  [
2393  'pt_namespace' => $this->mTitle->getNamespace(),
2394  'pt_title' => $this->mTitle->getDBkey(),
2395  'pt_create_perm' => $limit['create'],
2396  'pt_timestamp' => $dbw->timestamp(),
2397  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2398  'pt_user' => $user->getId(),
2399  ] + $commentFields, __METHOD__
2400  );
2401  $logParamsDetails[] = [
2402  'type' => 'create',
2403  'level' => $limit['create'],
2404  'expiry' => $expiry['create'],
2405  ];
2406  } else {
2407  $dbw->delete( 'protected_titles',
2408  [
2409  'pt_namespace' => $this->mTitle->getNamespace(),
2410  'pt_title' => $this->mTitle->getDBkey()
2411  ], __METHOD__
2412  );
2413  }
2414  }
2415 
2416  $this->mTitle->flushRestrictions();
2417  InfoAction::invalidateCache( $this->mTitle );
2418 
2419  if ( $logAction == 'unprotect' ) {
2420  $params = [];
2421  } else {
2422  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2423  $params = [
2424  '4::description' => $protectDescriptionLog, // parameter for IRC
2425  '5:bool:cascade' => $cascade,
2426  'details' => $logParamsDetails, // parameter for localize and api
2427  ];
2428  }
2429 
2430  // Update the protection log
2431  $logEntry = new ManualLogEntry( 'protect', $logAction );
2432  $logEntry->setTarget( $this->mTitle );
2433  $logEntry->setComment( $reason );
2434  $logEntry->setPerformer( $user );
2435  $logEntry->setParameters( $params );
2436  if ( $nullRevisionRecord !== null ) {
2437  $logEntry->setAssociatedRevId( $nullRevisionRecord->getId() );
2438  }
2439  $logEntry->addTags( $tags );
2440  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2441  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2442  }
2443  $logId = $logEntry->insert();
2444  $logEntry->publish( $logId );
2445 
2446  return Status::newGood( $logId );
2447  }
2448 
2463  string $revCommentMsg,
2464  array $limit,
2465  array $expiry,
2466  bool $cascade,
2467  string $reason,
2468  UserIdentity $user
2469  ): ?RevisionRecord {
2470  $dbw = wfGetDB( DB_PRIMARY );
2471 
2472  // Prepare a null revision to be added to the history
2473  $editComment = wfMessage(
2474  $revCommentMsg,
2475  $this->mTitle->getPrefixedText(),
2476  $user->getName()
2477  )->inContentLanguage()->text();
2478  if ( $reason ) {
2479  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2480  }
2481  $protectDescription = $this->protectDescription( $limit, $expiry );
2482  if ( $protectDescription ) {
2483  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2484  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2485  ->inContentLanguage()->text();
2486  }
2487  if ( $cascade ) {
2488  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2489  $editComment .= wfMessage( 'brackets' )->params(
2490  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2491  )->inContentLanguage()->text();
2492  }
2493 
2494  $revStore = $this->getRevisionStore();
2495  $comment = CommentStoreComment::newUnsavedComment( $editComment );
2496  $nullRevRecord = $revStore->newNullRevision(
2497  $dbw,
2498  $this->getTitle(),
2499  $comment,
2500  true,
2501  $user
2502  );
2503 
2504  if ( $nullRevRecord ) {
2505  $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
2506 
2507  // Update page record and touch page
2508  $oldLatest = $inserted->getParentId();
2509 
2510  $this->updateRevisionOn( $dbw, $inserted, $oldLatest );
2511 
2512  return $inserted;
2513  } else {
2514  return null;
2515  }
2516  }
2517 
2522  protected function formatExpiry( $expiry ) {
2523  if ( $expiry != 'infinity' ) {
2524  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2525  return wfMessage(
2526  'protect-expiring',
2527  $contLang->timeanddate( $expiry, false, false ),
2528  $contLang->date( $expiry, false, false ),
2529  $contLang->time( $expiry, false, false )
2530  )->inContentLanguage()->text();
2531  } else {
2532  return wfMessage( 'protect-expiry-indefinite' )
2533  ->inContentLanguage()->text();
2534  }
2535  }
2536 
2544  public function protectDescription( array $limit, array $expiry ) {
2545  $protectDescription = '';
2546 
2547  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2548  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2549  # All possible message keys are listed here for easier grepping:
2550  # * restriction-create
2551  # * restriction-edit
2552  # * restriction-move
2553  # * restriction-upload
2554  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2555  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2556  # with '' filtered out. All possible message keys are listed below:
2557  # * protect-level-autoconfirmed
2558  # * protect-level-sysop
2559  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2560  ->inContentLanguage()->text();
2561 
2562  $expiryText = $this->formatExpiry( $expiry[$action] );
2563 
2564  if ( $protectDescription !== '' ) {
2565  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2566  }
2567  $protectDescription .= wfMessage( 'protect-summary-desc' )
2568  ->params( $actionText, $restrictionsText, $expiryText )
2569  ->inContentLanguage()->text();
2570  }
2571 
2572  return $protectDescription;
2573  }
2574 
2586  public function protectDescriptionLog( array $limit, array $expiry ) {
2587  $protectDescriptionLog = '';
2588 
2589  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2590  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2591  $expiryText = $this->formatExpiry( $expiry[$action] );
2592  $protectDescriptionLog .=
2593  $dirMark .
2594  "[$action=$restrictions] ($expiryText)";
2595  }
2596 
2597  return trim( $protectDescriptionLog );
2598  }
2599 
2614  public function isBatchedDelete( $safetyMargin = 0 ) {
2616 
2617  $dbr = wfGetDB( DB_REPLICA );
2618  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2619  $revCount += $safetyMargin;
2620 
2621  return $revCount >= $wgDeleteRevisionsBatchSize;
2622  }
2623 
2654  public function doDeleteArticleReal(
2655  $reason, UserIdentity $deleter, $suppress = false, $u1 = null, &$error = '', $u2 = null,
2656  $tags = [], $logsubtype = 'delete', $immediate = false
2657  ) {
2658  $services = MediaWikiServices::getInstance();
2659  $deletePage = $services->getDeletePageFactory()->newDeletePage(
2660  $this,
2661  $services->getUserFactory()->newFromUserIdentity( $deleter )
2662  );
2663 
2664  $status = $deletePage
2665  ->setSuppress( $suppress )
2666  ->setTags( $tags ?: [] )
2667  ->setLogSubtype( $logsubtype )
2668  ->forceImmediate( $immediate )
2669  ->keepLegacyHookErrorsSeparate()
2670  ->deleteUnsafe( $reason );
2671  $error = $deletePage->getLegacyHookErrors();
2672  if ( $status->isGood() && $status->value === false ) {
2673  // BC for scheduled deletion
2674  $status->warning( 'delete-scheduled', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2675  $status->value = null;
2676  }
2677  return $status;
2678  }
2679 
2698  public function doDeleteArticleBatched(
2699  $reason, $suppress, UserIdentity $deleter, $tags,
2700  $logsubtype, $immediate = false, $webRequestId = null
2701  ) {
2702  $services = MediaWikiServices::getInstance();
2703  $deletePage = $services->getDeletePageFactory()->newDeletePage(
2704  $this,
2705  $services->getUserFactory()->newFromUserIdentity( $deleter )
2706  );
2707 
2708  $status = $deletePage
2709  ->setSuppress( $suppress )
2710  ->setTags( $tags )
2711  ->setLogSubtype( $logsubtype )
2712  ->forceImmediate( $immediate )
2713  ->deleteInternal( $reason, $webRequestId );
2714  if ( $status->isGood() && $status->value === false ) {
2715  // BC for scheduled deletion
2716  $status->warning( 'delete-scheduled', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2717  $status->value = null;
2718  }
2719  return $status;
2720  }
2721 
2728  public function lockAndGetLatest() {
2729  return (int)wfGetDB( DB_PRIMARY )->selectField(
2730  'page',
2731  'page_latest',
2732  [
2733  'page_id' => $this->getId(),
2734  // Typically page_id is enough, but some code might try to do
2735  // updates assuming the title is the same, so verify that
2736  'page_namespace' => $this->getTitle()->getNamespace(),
2737  'page_title' => $this->getTitle()->getDBkey()
2738  ],
2739  __METHOD__,
2740  [ 'FOR UPDATE' ]
2741  );
2742  }
2743 
2758  public function doDeleteUpdates(
2759  $id,
2760  Content $content = null,
2761  RevisionRecord $revRecord = null,
2762  UserIdentity $user = null
2763  ) {
2764  wfDeprecated( __METHOD__, '1.37' );
2765  if ( !$revRecord ) {
2766  throw new BadMethodCallException( __METHOD__ . ' now requires a RevisionRecord' );
2767  }
2768  if ( $id !== $this->getId() ) {
2769  throw new InvalidArgumentException( 'Mismatching page ID' );
2770  }
2771 
2772  $user = $user ?? new UserIdentityValue( 0, 'unknown' );
2773  $services = MediaWikiServices::getInstance();
2774  $deletePage = $services->getDeletePageFactory()->newDeletePage(
2775  $this,
2776  $services->getUserFactory()->newFromUserIdentity( $user )
2777  );
2778 
2779  $deletePage->doDeleteUpdates( $revRecord );
2780  }
2781 
2793  public static function onArticleCreate( Title $title ) {
2794  // TODO: move this into a PageEventEmitter service
2795 
2796  // Update existence markers on article/talk tabs...
2797  $other = $title->getOtherPage();
2798 
2799  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2800  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2801 
2802  $title->touchLinks();
2803  $title->deleteTitleProtection();
2804 
2805  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
2806 
2807  // Invalidate caches of articles which include this page
2809  $title,
2810  'templatelinks',
2811  [ 'causeAction' => 'page-create' ]
2812  );
2813  JobQueueGroup::singleton()->lazyPush( $job );
2814 
2815  if ( $title->getNamespace() === NS_CATEGORY ) {
2816  // Load the Category object, which will schedule a job to create
2817  // the category table row if necessary. Checking a replica DB is ok
2818  // here, in the worst case it'll run an unnecessary recount job on
2819  // a category that probably doesn't have many members.
2820  Category::newFromTitle( $title )->getID();
2821  }
2822  }
2823 
2829  public static function onArticleDelete( Title $title ) {
2830  // TODO: move this into a PageEventEmitter service
2831 
2832  // Update existence markers on article/talk tabs...
2833  $other = $title->getOtherPage();
2834 
2835  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2836  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2837 
2838  $title->touchLinks();
2839 
2840  $services = MediaWikiServices::getInstance();
2841  $services->getLinkCache()->invalidateTitle( $title );
2842 
2844 
2845  // Messages
2846  if ( $title->getNamespace() === NS_MEDIAWIKI ) {
2847  $services->getMessageCache()->updateMessageOverride( $title, null );
2848  }
2849 
2850  // Images
2851  if ( $title->getNamespace() === NS_FILE ) {
2853  $title,
2854  'imagelinks',
2855  [ 'causeAction' => 'page-delete' ]
2856  );
2857  JobQueueGroup::singleton()->lazyPush( $job );
2858  }
2859 
2860  // User talk pages
2861  if ( $title->getNamespace() === NS_USER_TALK ) {
2862  $user = User::newFromName( $title->getText(), false );
2863  if ( $user ) {
2864  MediaWikiServices::getInstance()
2865  ->getTalkPageNotificationManager()
2866  ->removeUserHasNewMessages( $user );
2867  }
2868  }
2869 
2870  // Image redirects
2871  $services->getRepoGroup()->getLocalRepo()->invalidateImageRedirect( $title );
2872 
2873  // Purge cross-wiki cache entities referencing this page
2875  }
2876 
2885  public static function onArticleEdit(
2886  Title $title,
2887  RevisionRecord $revRecord = null,
2888  $slotsChanged = null
2889  ) {
2890  // TODO: move this into a PageEventEmitter service
2891 
2892  $jobs = [];
2893  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
2894  // Invalidate caches of articles which include this page.
2895  // Only for the main slot, because only the main slot is transcluded.
2896  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
2898  $title,
2899  'templatelinks',
2900  [ 'causeAction' => 'page-edit' ]
2901  );
2902  }
2903  // Invalidate the caches of all pages which redirect here
2905  $title,
2906  'redirect',
2907  [ 'causeAction' => 'page-edit' ]
2908  );
2909  JobQueueGroup::singleton()->lazyPush( $jobs );
2910 
2911  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
2912 
2913  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2914  $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2915 
2916  // Purge ?action=info cache
2917  $revid = $revRecord ? $revRecord->getId() : null;
2918  DeferredUpdates::addCallableUpdate( static function () use ( $title, $revid ) {
2920  } );
2921 
2922  // Purge cross-wiki cache entities referencing this page
2924  }
2925 
2933  private static function purgeInterwikiCheckKey( Title $title ) {
2935 
2936  if ( !$wgEnableScaryTranscluding ) {
2937  return; // @todo: perhaps this wiki is only used as a *source* for content?
2938  }
2939 
2940  DeferredUpdates::addCallableUpdate( static function () use ( $title ) {
2941  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2942  $cache->resetCheckKey(
2943  // Do not include the namespace since there can be multiple aliases to it
2944  // due to different namespace text definitions on different wikis. This only
2945  // means that some cache invalidations happen that are not strictly needed.
2946  $cache->makeGlobalKey(
2947  'interwiki-page',
2949  $title->getDBkey()
2950  )
2951  );
2952  } );
2953  }
2954 
2961  public function getCategories() {
2962  $id = $this->getId();
2963  if ( $id == 0 ) {
2964  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
2965  }
2966 
2967  $dbr = wfGetDB( DB_REPLICA );
2968  $res = $dbr->select( 'categorylinks',
2969  [ 'page_title' => 'cl_to', 'page_namespace' => NS_CATEGORY ],
2970  [ 'cl_from' => $id ],
2971  __METHOD__
2972  );
2973 
2974  return TitleArray::newFromResult( $res );
2975  }
2976 
2983  public function getHiddenCategories() {
2984  $result = [];
2985  $id = $this->getId();
2986 
2987  if ( $id == 0 ) {
2988  return [];
2989  }
2990 
2991  $dbr = wfGetDB( DB_REPLICA );
2992  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
2993  [ 'cl_to' ],
2994  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
2995  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
2996  __METHOD__ );
2997 
2998  if ( $res !== false ) {
2999  foreach ( $res as $row ) {
3000  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3001  }
3002  }
3003 
3004  return $result;
3005  }
3006 
3014  public function getAutoDeleteReason( &$hasHistory ) {
3015  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3016  }
3017 
3028  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3029  $id = $id ?: $this->getId();
3030  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3031  getCategoryLinkType( $this->getTitle()->getNamespace() );
3032 
3033  $addFields = [ 'cat_pages = cat_pages + 1' ];
3034  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3035  if ( $type !== 'page' ) {
3036  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3037  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3038  }
3039 
3040  $dbw = wfGetDB( DB_PRIMARY );
3041 
3042  if ( count( $added ) ) {
3043  $existingAdded = $dbw->selectFieldValues(
3044  'category',
3045  'cat_title',
3046  [ 'cat_title' => $added ],
3047  __METHOD__
3048  );
3049 
3050  // For category rows that already exist, do a plain
3051  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3052  // to avoid creating gaps in the cat_id sequence.
3053  if ( count( $existingAdded ) ) {
3054  $dbw->update(
3055  'category',
3056  $addFields,
3057  [ 'cat_title' => $existingAdded ],
3058  __METHOD__
3059  );
3060  }
3061 
3062  $missingAdded = array_diff( $added, $existingAdded );
3063  if ( count( $missingAdded ) ) {
3064  $insertRows = [];
3065  foreach ( $missingAdded as $cat ) {
3066  $insertRows[] = [
3067  'cat_title' => $cat,
3068  'cat_pages' => 1,
3069  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3070  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3071  ];
3072  }
3073  $dbw->upsert(
3074  'category',
3075  $insertRows,
3076  'cat_title',
3077  $addFields,
3078  __METHOD__
3079  );
3080  }
3081  }
3082 
3083  if ( count( $deleted ) ) {
3084  $dbw->update(
3085  'category',
3086  $removeFields,
3087  [ 'cat_title' => $deleted ],
3088  __METHOD__
3089  );
3090  }
3091 
3092  foreach ( $added as $catName ) {
3093  $cat = Category::newFromName( $catName );
3094  $this->getHookRunner()->onCategoryAfterPageAdded( $cat, $this );
3095  }
3096 
3097  foreach ( $deleted as $catName ) {
3098  $cat = Category::newFromName( $catName );
3099  $this->getHookRunner()->onCategoryAfterPageRemoved( $cat, $this, $id );
3100  // Refresh counts on categories that should be empty now (after commit, T166757)
3101  DeferredUpdates::addCallableUpdate( static function () use ( $cat ) {
3102  $cat->refreshCountsIfEmpty();
3103  } );
3104  }
3105  }
3106 
3119  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3120  if ( wfReadOnly() ) {
3121  return;
3122  }
3123 
3124  if ( !$this->getHookRunner()->onOpportunisticLinksUpdate( $this,
3125  $this->mTitle, $parserOutput )
3126  ) {
3127  return;
3128  }
3129 
3130  $config = RequestContext::getMain()->getConfig();
3131 
3132  $params = [
3133  'isOpportunistic' => true,
3134  'rootJobTimestamp' => $parserOutput->getCacheTime()
3135  ];
3136 
3137  if ( $this->mTitle->areRestrictionsCascading() ) {
3138  // In general, MediaWiki does not re-run LinkUpdate (e.g. for search index, category
3139  // listings, and backlinks for Whatlinkshere), unless either the page was directly
3140  // edited, or was re-generate following a template edit propagating to an affected
3141  // page. As such, during page views when there is no valid ParserCache entry,
3142  // we re-parse and save, but leave indexes as-is.
3143  //
3144  // We make an exception for pages that have cascading protection (perhaps for a wiki's
3145  // "Main Page"). When such page is re-parsed on-demand after a parser cache miss, we
3146  // queue a high-priority LinksUpdate job, to ensure that we really protect all
3147  // content that is currently transcluded onto the page. This is important, because
3148  // wikitext supports conditional statements based on the current time, which enables
3149  // transcluding of a different sub page based on which day it is, and then show that
3150  // information on the Main Page, without the Main Page itself being edited.
3151  JobQueueGroup::singleton()->lazyPush(
3152  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3153  );
3154  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasReducedExpiry() ) {
3155  // Assume the output contains "dynamic" time/random based magic words.
3156  // Only update pages that expired due to dynamic content and NOT due to edits
3157  // to referenced templates/files. When the cache expires due to dynamic content,
3158  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3159  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3160  // template/file edit already triggered recursive RefreshLinksJob jobs.
3161  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3162  // If a page is uncacheable, do not keep spamming a job for it.
3163  // Although it would be de-duplicated, it would still waste I/O.
3165  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3166  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3167  if ( $cache->add( $key, time(), $ttl ) ) {
3168  JobQueueGroup::singleton()->lazyPush(
3169  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3170  );
3171  }
3172  }
3173  }
3174  }
3175 
3187  public function getDeletionUpdates( $rev = null ) {
3188  wfDeprecated( __METHOD__, '1.37' );
3189  $user = new UserIdentityValue( 0, 'Legacy code hater' );
3190  $services = MediaWikiServices::getInstance();
3191  $deletePage = $services->getDeletePageFactory()->newDeletePage(
3192  $this,
3193  $services->getUserFactory()->newFromUserIdentity( $user )
3194  );
3195 
3196  if ( !$rev ) {
3197  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3198 
3199  try {
3200  $rev = $this->getRevisionRecord();
3201  } catch ( Exception $ex ) {
3202  // If we can't load the content, something is wrong. Perhaps that's why
3203  // the user is trying to delete the page, so let's not fail in that case.
3204  // Note that doDeleteArticleReal() will already have logged an issue with
3205  // loading the content.
3206  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3207  }
3208  }
3209  if ( !$rev ) {
3210  // Use an empty RevisionRecord
3211  $newRev = new MutableRevisionRecord( $this );
3212  } elseif ( $rev instanceof Content ) {
3213  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3214  $newRev = new MutableRevisionRecord( $this );
3215  $newRev->setSlot( SlotRecord::newUnsaved( SlotRecord::MAIN, $rev ) );
3216  } else {
3217  $newRev = $rev;
3218  }
3219  return $deletePage->getDeletionUpdates( $newRev );
3220  }
3221 
3229  public function isLocal() {
3230  return true;
3231  }
3232 
3242  public function getWikiDisplayName() {
3243  global $wgSitename;
3244  return $wgSitename;
3245  }
3246 
3255  public function getSourceURL() {
3256  return $this->getTitle()->getCanonicalURL();
3257  }
3258 
3265  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3266 
3267  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3268  }
3269 
3276  public function __wakeup() {
3277  // Make sure we re-fetch the latest state from the database.
3278  // In particular, the latest revision may have changed.
3279  // As a side-effect, this makes sure mLastRevision doesn't
3280  // end up being an instance of the old Revision class (see T259181),
3281  // especially since that class was removed entirely in 1.37.
3282  $this->clear();
3283  }
3284 
3289  public function getNamespace(): int {
3290  return $this->getTitle()->getNamespace();
3291  }
3292 
3297  public function getDBkey(): string {
3298  return $this->getTitle()->getDBkey();
3299  }
3300 
3305  public function getWikiId() {
3306  return $this->getTitle()->getWikiId();
3307  }
3308 
3313  public function canExist(): bool {
3314  return true;
3315  }
3316 
3321  public function __toString(): string {
3322  return $this->mTitle->__toString();
3323  }
3324 
3332  public function isSamePageAs( PageReference $other ): bool {
3333  // NOTE: keep in sync with PageIdentityValue::isSamePageAs()!
3334 
3335  if ( $other->getWikiId() !== $this->getWikiId() ) {
3336  return false;
3337  }
3338 
3339  if ( $other->getNamespace() !== $this->getNamespace()
3340  || $other->getDBkey() !== $this->getDBkey() ) {
3341  return false;
3342  }
3343 
3344  return true;
3345  }
3346 
3358  public function toPageRecord(): ExistingPageRecord {
3359  // TODO: replace individual member fields with a PageRecord instance that is always present
3360 
3361  if ( !$this->mDataLoaded ) {
3362  $this->loadPageData();
3363  }
3364 
3365  Assert::precondition(
3366  $this->exists(),
3367  'This WikiPage instance does not represent an existing page: ' . $this->mTitle
3368  );
3369 
3370  return new PageStoreRecord(
3371  (object)[
3372  'page_id' => $this->getId(),
3373  'page_namespace' => $this->mTitle->getNamespace(),
3374  'page_title' => $this->mTitle->getDBkey(),
3375  'page_latest' => $this->mLatest,
3376  'page_is_new' => $this->mIsNew,
3377  'page_is_redirect' => $this->mIsRedirect,
3378  'page_touched' => $this->getTouched(),
3379  'page_lang' => $this->getLanguage()
3380  ],
3381  PageIdentity::LOCAL
3382  );
3383  }
3384 
3385 }
WikiPage\getCategories
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:2961
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:638
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:3358
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:142
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
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:647
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:1914
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:2793
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:466
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\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3014
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:816
ParserOutput
Definition: ParserOutput.php:36
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:1023
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:937
WikiPage\getWikiId
getWikiId()
Definition: WikiPage.php:3305
WikiPage\clearCacheFields
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:330
Title\getFragment
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1772
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:2614
WikiPage\wasLoadedFrom
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:511
TitleArray\newFromResult
static newFromResult( $res)
Definition: TitleArray.php:44
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1273
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:199
Page\ParserOutputAccess
Service for getting rendered output of a given page.
Definition: ParserOutputAccess.php:50
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
WikiPage\hasViewableContent
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition: WikiPage.php:611
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:718
WikiPage\__toString
__toString()
Returns an informative human readable unique representation of the page identity, for use as a cache ...
Definition: WikiPage.php:3321
WikiPage\getUserText
getUserText( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:916
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1609
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1655
WikiPage\getLanguage
getLanguage()
Definition: WikiPage.php:728
WikiPage\$mDataLoadedFrom
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:134
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:60
WikiPage\replaceSectionContent
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1570
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1996
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1158
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1098
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:606
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1183
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:1199
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:2089
WikiPage\doViewUpdates
doViewUpdates(Authority $performer, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1299
$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:1572
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\prepareContentForEdit
prepareContentForEdit(Content $content, RevisionRecord $revision=null, UserIdentity $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:2027
WikiPage\getDBLoadBalancer
getDBLoadBalancer()
Definition: WikiPage.php:279
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:289
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:8073
WikiPage\$mLanguage
string null $mLanguage
Definition: WikiPage.php:154
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:8089
RefreshLinksJob\newPrioritized
static newPrioritized(PageIdentity $page, array $params)
Definition: RefreshLinksJob.php:70
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:72
WikiPage\$mTouched
string $mTouched
Definition: WikiPage.php:149
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:3119
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:2544
Title\castFromPageIdentity
static castFromPageIdentity(?PageIdentity $pageIdentity)
Return a Title for a given PageIdentity.
Definition: Title.php:331
$dbr
$dbr
Definition: testCompression.php:54
WikiPage\updateParserCache
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2119
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1552
$wgEnableScaryTranscluding
$wgEnableScaryTranscluding
Enable interwiki transcluding.
Definition: DefaultSettings.php:5062
WikiPage\doDeleteArticleBatched
doDeleteArticleBatched( $reason, $suppress, UserIdentity $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2698
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:1059
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:1407
WikiPage\isSamePageAs
isSamePageAs(PageReference $other)
Checks whether the given PageReference refers to the same page as this PageReference....
Definition: WikiPage.php:3332
WikiPage\getDBkey
getDBkey()
Get the page title in DB key form.This should always return a valid DB key.string
Definition: WikiPage.php:3297
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:952
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:1534
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1068
WikiPage\onArticleEdit
static onArticleEdit(Title $title, RevisionRecord $revRecord=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:2885
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:2163
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2200
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:354
Title\getInterwiki
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:969
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1362
Title\isValidRedirectTarget
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition: Title.php:3856
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1250
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:311
WikiPage\exists
exists()
Definition: WikiPage.php:596
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:185
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:2829
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:125
WikiPage\$mRedirectTarget
Title $mRedirectTarget
The cache of the redirect target.
Definition: WikiPage.php:100
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:710
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:740
WikiPage\purgeInterwikiCheckKey
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:2933
WikiPage\$mDataLoaded
bool $mDataLoaded
Definition: WikiPage.php:79
ParserOptions\newCanonical
static newCanonical( $context, $userLang=null)
Creates a "canonical" ParserOptions object.
Definition: ParserOptions.php:1091
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:2654
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:650
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:862
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:1694
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:117
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:1327
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:969
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:671
WikiPage\$mPageIsRedirectField
bool $mPageIsRedirectField
A cache of the page_is_redirect field, loaded with page data.
Definition: WikiPage.php:85
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:432
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2728
MediaWiki\Revision\RevisionRecord\getSlots
getSlots()
Returns the slots defined for this revision.
Definition: RevisionRecord.php:222
$wgPageLanguageUseDB
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
Definition: DefaultSettings.php:2552
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:676
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:93
WikiPage\getId
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:584
$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:3187
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:2586
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:1076
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition: WikiPage.php:837
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:79
HTMLCacheUpdateJob\newForBacklinks
static newForBacklinks(PageReference $page, $table, $params=[])
Definition: HTMLCacheUpdateJob.php:61
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:137
WikiPage\newFromID
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:215
WikiPage\isNew
isNew()
Tests if the page is new (only has one revision).
Definition: WikiPage.php:653
$wgSitename
$wgSitename
Name of the site.
Definition: DefaultSettings.php:82
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:3255
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1456
WikiPage\$derivedDataUpdater
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:164
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:1836
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:2983
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:231
WikiPage\$mLastRevision
RevisionRecord null $mLastRevision
Definition: WikiPage.php:139
WikiPage\canExist
canExist()
Definition: WikiPage.php:3313
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:93
$wgDeleteRevisionsBatchSize
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
Definition: DefaultSettings.php:6380
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:367
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:765
WikiPage\setLastEdit
setLastEdit(RevisionRecord $revRecord)
Set the latest revision.
Definition: WikiPage.php:804
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:2462
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1147
WikiPage\getPageUpdaterFactory
getPageUpdaterFactory()
Definition: WikiPage.php:258
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:537
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:6174
RefreshLinksJob\newDynamic
static newDynamic(PageIdentity $page, array $params)
Definition: RefreshLinksJob.php:82
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2522
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:110
InfoAction\invalidateCache
static invalidateCache(PageIdentity $page, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:168
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:1113
WikiPage\getUser
getUser( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:876
$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:200
WikiPage\doDeleteUpdates
doDeleteUpdates( $id, Content $content=null, RevisionRecord $revRecord=null, UserIdentity $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:2758
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:49
WikiPage\$mId
int $mId
Definition: WikiPage.php:129
WikiPage\getWikiDisplayName
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3242
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:241
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:3028
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3264
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:752
MediaWiki\Storage\PageUpdaterFactory
A factory for PageUpdater instances.
Definition: PageUpdaterFactory.php:54
$source
$source
Definition: mwdoc-filter.php:34
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:44
WikiPage\pageData
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:403
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:3229
$wgArticleCountMethod
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article.
Definition: DefaultSettings.php:5104
Category\newFromName
static newFromName( $name)
Factory function.
Definition: Category.php:139
WikiPage\insertRedirectEntry
insertRedirectEntry(Title $rt, $oldLatest=null)
Insert or update the redirect table entry for this page to indicate it redirects to $rt.
Definition: WikiPage.php:1102
WikiPage\newPageUpdater
newPageUpdater( $performer, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
Definition: WikiPage.php:1756
NS_FILE
const NS_FILE
Definition: Defines.php:70
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:848
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1495
WikiPage\$mPreparedEdit
PreparedEdit false $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:124
WikiPage\$mLinksUpdated
string $mLinksUpdated
Definition: WikiPage.php:159
WikiPage\isRedirect
isRedirect()
Is the page a redirect, according to secondary tracking tables? If this is true, getRedirectTarget() ...
Definition: WikiPage.php:621
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:127
WikiPage\__construct
__construct(PageIdentity $pageIdentity)
Definition: WikiPage.php:169
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:105
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:65
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:145
WikiPage\getCreator
getCreator( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:897
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:450
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:302
WikiPage\__wakeup
__wakeup()
Ensure consistency when unserializing.
Definition: WikiPage.php:3276
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:144
WikiPage\$mIsNew
bool $mIsNew
Definition: WikiPage.php:105
Title\purgeExpiredRestrictions
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:2693
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:3289
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:2193
WikiPage\getContentHandlerFactory
getContentHandlerFactory()
Definition: WikiPage.php:272
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:319
WikiPage\getRevisionStore
getRevisionStore()
Definition: WikiPage.php:265
$type
$type
Definition: testCompression.php:52