MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
27 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
46 use Wikimedia\Assert\Assert;
47 use Wikimedia\Assert\PreconditionException;
48 use Wikimedia\IPUtils;
49 use Wikimedia\NonSerializable\NonSerializableTrait;
53 
61  use NonSerializableTrait;
62  use ProtectedHookAccessorTrait;
64 
65  // Constants for $mDataLoadedFrom and related
66 
72  public $mTitle = null;
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  // TODO: In order to allow WikiPage to implement ProperPageIdentity,
176  // throw here to prevent construction of a WikiPage that doesn't
177  // represent a proper page.
179  "WikiPage constructed on a Title that cannot exist as a page: $title",
180  '1.36'
181  );
182  }
183 
184  $this->mTitle = $title;
185  }
186 
191  public function __clone() {
192  $this->mTitle = clone $this->mTitle;
193  }
194 
206  public static function factory( PageIdentity $pageIdentity ) {
207  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $pageIdentity );
208  }
209 
221  public static function newFromID( $id, $from = 'fromdb' ) {
222  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromID( $id, $from );
223  }
224 
237  public static function newFromRow( $row, $from = 'fromdb' ) {
238  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromRow( $row, $from );
239  }
240 
247  public static function convertSelectType( $type ) {
248  switch ( $type ) {
249  case 'fromdb':
250  return self::READ_NORMAL;
251  case 'fromdbmaster':
252  return self::READ_LATEST;
253  case 'forupdate':
254  return self::READ_LOCKING;
255  default:
256  // It may already be an integer or whatever else
257  return $type;
258  }
259  }
260 
265  return MediaWikiServices::getInstance()->getPageUpdaterFactory();
266  }
267 
271  private function getRevisionStore() {
272  return MediaWikiServices::getInstance()->getRevisionStore();
273  }
274 
279  return MediaWikiServices::getInstance()->getContentHandlerFactory();
280  }
281 
285  private function getDBLoadBalancer() {
286  return MediaWikiServices::getInstance()->getDBLoadBalancer();
287  }
288 
295  public function getActionOverrides() {
296  return $this->getContentHandler()->getActionOverrides();
297  }
298 
308  public function getContentHandler() {
309  return $this->getContentHandlerFactory()
310  ->getContentHandler( $this->getContentModel() );
311  }
312 
317  public function getTitle() {
318  return $this->mTitle;
319  }
320 
325  public function clear() {
326  $this->mDataLoaded = false;
327  $this->mDataLoadedFrom = self::READ_NONE;
328 
329  $this->clearCacheFields();
330  }
331 
336  protected function clearCacheFields() {
337  $this->mId = null;
338  $this->mRedirectTarget = null; // Title object if set
339  $this->mHasRedirectTarget = null;
340  $this->mPageIsRedirectField = false;
341  $this->mLastRevision = null; // Latest revision
342  $this->mTouched = '19700101000000';
343  $this->mLanguage = null;
344  $this->mLinksUpdated = '19700101000000';
345  $this->mTimestamp = '';
346  $this->mIsNew = false;
347  $this->mIsRedirect = false;
348  $this->mLatest = false;
349  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
350  // checks the requested rev ID and content against the cached one. For most
351  // content types, the output should not change during the lifetime of this cache.
352  // Clearing it can cause extra parses on edit for no reason.
353  }
354 
360  public function clearPreparedEdit() {
361  $this->mPreparedEdit = false;
362  }
363 
373  public static function getQueryInfo() {
374  global $wgPageLanguageUseDB;
375 
376  $ret = [
377  'tables' => [ 'page' ],
378  'fields' => [
379  'page_id',
380  'page_namespace',
381  'page_title',
382  'page_restrictions',
383  'page_is_redirect',
384  'page_is_new',
385  'page_random',
386  'page_touched',
387  'page_links_updated',
388  'page_latest',
389  'page_len',
390  'page_content_model',
391  ],
392  'joins' => [],
393  ];
394 
395  if ( $wgPageLanguageUseDB ) {
396  $ret['fields'][] = 'page_lang';
397  }
398 
399  return $ret;
400  }
401 
409  protected function pageData( $dbr, $conditions, $options = [] ) {
410  $pageQuery = self::getQueryInfo();
411 
412  $this->getHookRunner()->onArticlePageDataBefore(
413  $this, $pageQuery['fields'], $pageQuery['tables'], $pageQuery['joins'] );
414 
415  $row = $dbr->selectRow(
416  $pageQuery['tables'],
417  $pageQuery['fields'],
418  $conditions,
419  __METHOD__,
420  $options,
421  $pageQuery['joins']
422  );
423 
424  $this->getHookRunner()->onArticlePageDataAfter( $this, $row );
425 
426  return $row;
427  }
428 
438  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
439  if ( !$title->canExist() ) {
440  return false;
441  }
442 
443  return $this->pageData( $dbr, [
444  'page_namespace' => $title->getNamespace(),
445  'page_title' => $title->getDBkey() ], $options );
446  }
447 
456  public function pageDataFromId( $dbr, $id, $options = [] ) {
457  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
458  }
459 
472  public function loadPageData( $from = 'fromdb' ) {
473  if ( !$this->mTitle->canExist() ) {
474  // NOTE: If and when WikiPage implements PageIdentity but not yet ProperPageIdentity,
475  // throw here to prevent usage of a WikiPage that doesn't
476  // represent a proper page.
477  // NOTE: The constructor will already have triggered a warning, but seeing how
478  // bad instances of WikiPage are used will be helpful.
480  "Accessing WikiPage that cannot exist as a page: {$this->mTitle}. ",
481  '1.36'
482  );
483  }
484 
485  $from = self::convertSelectType( $from );
486  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
487  // We already have the data from the correct location, no need to load it twice.
488  return;
489  }
490 
491  if ( is_int( $from ) ) {
492  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
493  $loadBalancer = $this->getDBLoadBalancer();
494  $db = $loadBalancer->getConnection( $index );
495  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
496 
497  if ( !$data
498  && $index == DB_REPLICA
499  && $loadBalancer->getServerCount() > 1
500  && $loadBalancer->hasOrMadeRecentMasterChanges()
501  ) {
502  $from = self::READ_LATEST;
503  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
504  $db = $loadBalancer->getConnection( $index );
505  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
506  }
507  } else {
508  // No idea from where the caller got this data, assume replica DB.
509  $data = $from;
510  $from = self::READ_NORMAL;
511  }
512 
513  $this->loadFromRow( $data, $from );
514  }
515 
529  public function wasLoadedFrom( $from ) {
530  $from = self::convertSelectType( $from );
531 
532  if ( !is_int( $from ) ) {
533  // No idea from where the caller got this data, assume replica DB.
534  $from = self::READ_NORMAL;
535  }
536 
537  if ( $from <= $this->mDataLoadedFrom ) {
538  return true;
539  }
540 
541  return false;
542  }
543 
555  public function loadFromRow( $data, $from ) {
556  $lc = MediaWikiServices::getInstance()->getLinkCache();
557  $lc->clearLink( $this->mTitle );
558 
559  if ( $data ) {
560  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
561 
562  $this->mTitle->loadFromRow( $data );
563 
564  // Old-fashioned restrictions
565  $this->mTitle->loadRestrictions( $data->page_restrictions );
566 
567  $this->mId = intval( $data->page_id );
568  $this->mTouched = MWTimestamp::convert( TS_MW, $data->page_touched );
569  $this->mLanguage = $data->page_lang ?? null;
570  $this->mLinksUpdated = $data->page_links_updated === null
571  ? null
572  : MWTimestamp::convert( TS_MW, $data->page_links_updated );
573  $this->mPageIsRedirectField = (bool)$data->page_is_redirect;
574  $this->mIsNew = intval( $data->page_is_new ?? 0 );
575  $this->mIsRedirect = intval( $data->page_is_redirect ?? 0 );
576  $this->mLatest = intval( $data->page_latest );
577  // T39225: $latest may no longer match the cached latest RevisionRecord object.
578  // Double-check the ID of any cached latest RevisionRecord object for consistency.
579  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
580  $this->mLastRevision = null;
581  $this->mTimestamp = '';
582  }
583  } else {
584  $lc->addBadLinkObj( $this->mTitle );
585 
586  $this->mTitle->loadFromRow( false );
587 
588  $this->clearCacheFields();
589 
590  $this->mId = 0;
591  }
592 
593  $this->mDataLoaded = true;
594  $this->mDataLoadedFrom = self::convertSelectType( $from );
595  }
596 
608  private function assertProperPage() {
609  Assert::precondition(
610  $this->mTitle->canExist(),
611  'This WikiPage instance does not represent a proper page!'
612  );
613  }
614 
620  public function getId( $wikiId = self::LOCAL ): int {
621  $this->assertWiki( $wikiId );
622  $this->assertProperPage();
623 
624  if ( !$this->mDataLoaded ) {
625  $this->loadPageData();
626  }
627  return $this->mId;
628  }
629 
633  public function exists(): bool {
634  if ( !$this->mDataLoaded ) {
635  $this->loadPageData();
636  }
637  return $this->mId > 0;
638  }
639 
648  public function hasViewableContent() {
649  return $this->mTitle->isKnown();
650  }
651 
658  public function isRedirect() {
659  if ( !$this->mDataLoaded ) {
660  $this->loadPageData();
661  }
662 
663  return (bool)$this->mIsRedirect;
664  }
665 
675  public function getPageIsRedirectField() {
676  if ( !$this->mDataLoaded ) {
677  $this->loadPageData();
678  }
680  }
681 
690  public function isNew() {
691  if ( !$this->mDataLoaded ) {
692  $this->loadPageData();
693  }
694 
695  return (bool)$this->mIsNew;
696  }
697 
708  public function getContentModel() {
709  if ( $this->exists() ) {
710  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
711 
712  return $cache->getWithSetCallback(
713  $cache->makeKey( 'page-content-model', $this->getLatest() ),
714  $cache::TTL_MONTH,
715  function () {
716  $rev = $this->getRevisionRecord();
717  if ( $rev ) {
718  // Look at the revision's actual content model
719  $slot = $rev->getSlot(
720  SlotRecord::MAIN,
721  RevisionRecord::RAW
722  );
723  return $slot->getModel();
724  } else {
725  LoggerFactory::getInstance( 'wikipage' )->warning(
726  'Page exists but has no (visible) revisions!',
727  [
728  'page-title' => $this->mTitle->getPrefixedDBkey(),
729  'page-id' => $this->getId(),
730  ]
731  );
732  return $this->mTitle->getContentModel();
733  }
734  },
735  [ 'pcTTL' => $cache::TTL_PROC_LONG ]
736  );
737  }
738 
739  // use the default model for this page
740  return $this->mTitle->getContentModel();
741  }
742 
747  public function checkTouched() {
748  return ( $this->exists() && !$this->isRedirect() );
749  }
750 
755  public function getTouched() {
756  if ( !$this->mDataLoaded ) {
757  $this->loadPageData();
758  }
759  return $this->mTouched;
760  }
761 
765  public function getLanguage() {
766  if ( !$this->mDataLoaded ) {
767  $this->loadLastEdit();
768  }
769 
770  return $this->mLanguage;
771  }
772 
777  public function getLinksTimestamp() {
778  if ( !$this->mDataLoaded ) {
779  $this->loadPageData();
780  }
781  return $this->mLinksUpdated;
782  }
783 
789  public function getLatest( $wikiId = self::LOCAL ) {
790  $this->assertWiki( $wikiId );
791 
792  if ( !$this->mDataLoaded ) {
793  $this->loadPageData();
794  }
795  return (int)$this->mLatest;
796  }
797 
802  protected function loadLastEdit() {
803  if ( $this->mLastRevision !== null ) {
804  return; // already loaded
805  }
806 
807  if ( !$this->mTitle->canExist() ) {
808  // NOTE: If and when WikiPage implements PageIdentity but not yet ProperPageIdentity,
809  // throw here to prevent usage of a WikiPage that doesn't
810  // represent a proper page.
811  // NOTE: The constructor will already have triggered a warning, but seeing how
812  // bad instances of WikiPage are used will be helpful.
814  "Accessing WikiPage that cannot exist as a page: {$this->mTitle}. ",
815  '1.36'
816  );
817  }
818 
819  $latest = $this->getLatest();
820  if ( !$latest ) {
821  return; // page doesn't exist or is missing page_latest info
822  }
823 
824  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
825  // T39225: if session S1 loads the page row FOR UPDATE, the result always
826  // includes the latest changes committed. This is true even within REPEATABLE-READ
827  // transactions, where S1 normally only sees changes committed before the first S1
828  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
829  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
830  // happened after the first S1 SELECT.
831  // https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html#isolevel_repeatable-read
832  $revision = $this->getRevisionStore()
833  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LOCKING );
834  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
835  // Bug T93976: if page_latest was loaded from the master, fetch the
836  // revision from there as well, as it may not exist yet on a replica DB.
837  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
838  $revision = $this->getRevisionStore()
839  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LATEST );
840  } else {
841  $revision = $this->getRevisionStore()->getKnownCurrentRevision( $this->getTitle(), $latest );
842  }
843 
844  if ( $revision ) { // sanity
845  $this->setLastEdit( $revision );
846  }
847  }
848 
853  private function setLastEdit( RevisionRecord $revRecord ) {
854  $this->mLastRevision = $revRecord;
855  $this->mLatest = $revRecord->getId();
856  $this->mTimestamp = $revRecord->getTimestamp();
857  $this->mTouched = max( $this->mTouched, $revRecord->getTimestamp() );
858  }
859 
865  public function getRevisionRecord() {
866  $this->loadLastEdit();
867  if ( $this->mLastRevision ) {
868  return $this->mLastRevision;
869  }
870  return null;
871  }
872 
886  public function getContent( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
887  $this->loadLastEdit();
888  if ( $this->mLastRevision ) {
889  return $this->mLastRevision->getContent( SlotRecord::MAIN, $audience, $performer );
890  }
891  return null;
892  }
893 
897  public function getTimestamp() {
898  // Check if the field has been filled by WikiPage::setTimestamp()
899  if ( !$this->mTimestamp ) {
900  $this->loadLastEdit();
901  }
902 
903  return MWTimestamp::convert( TS_MW, $this->mTimestamp );
904  }
905 
911  public function setTimestamp( $ts ) {
912  $this->mTimestamp = MWTimestamp::convert( TS_MW, $ts );
913  }
914 
925  public function getUser( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
926  $this->loadLastEdit();
927  if ( $this->mLastRevision ) {
928  $revUser = $this->mLastRevision->getUser( $audience, $performer );
929  return $revUser ? $revUser->getId() : 0;
930  } else {
931  return -1;
932  }
933  }
934 
946  public function getCreator( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
947  $revRecord = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
948  if ( $revRecord ) {
949  return $revRecord->getUser( $audience, $performer );
950  } else {
951  return null;
952  }
953  }
954 
965  public function getUserText( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
966  $this->loadLastEdit();
967  if ( $this->mLastRevision ) {
968  $revUser = $this->mLastRevision->getUser( $audience, $performer );
969  return $revUser ? $revUser->getName() : '';
970  } else {
971  return '';
972  }
973  }
974 
986  public function getComment( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
987  $this->loadLastEdit();
988  if ( $this->mLastRevision ) {
989  $revComment = $this->mLastRevision->getComment( $audience, $performer );
990  return $revComment ? $revComment->text : '';
991  } else {
992  return '';
993  }
994  }
995 
1001  public function getMinorEdit() {
1002  $this->loadLastEdit();
1003  if ( $this->mLastRevision ) {
1004  return $this->mLastRevision->isMinor();
1005  } else {
1006  return false;
1007  }
1008  }
1009 
1018  public function isCountable( $editInfo = false ) {
1019  global $wgArticleCountMethod;
1020 
1021  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
1022 
1023  if ( !$this->mTitle->isContentPage() ) {
1024  return false;
1025  }
1026 
1027  if ( $editInfo ) {
1028  // NOTE: only the main slot can make a page a redirect
1029  $content = $editInfo->pstContent;
1030  } else {
1031  $content = $this->getContent();
1032  }
1033 
1034  if ( !$content || $content->isRedirect() ) {
1035  return false;
1036  }
1037 
1038  $hasLinks = null;
1039 
1040  if ( $wgArticleCountMethod === 'link' ) {
1041  // nasty special case to avoid re-parsing to detect links
1042 
1043  if ( $editInfo ) {
1044  // ParserOutput::getLinks() is a 2D array of page links, so
1045  // to be really correct we would need to recurse in the array
1046  // but the main array should only have items in it if there are
1047  // links.
1048  $hasLinks = (bool)count( $editInfo->output->getLinks() );
1049  } else {
1050  // NOTE: keep in sync with RevisionRenderer::getLinkCount
1051  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
1052  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', '1',
1053  [ 'pl_from' => $this->getId() ], __METHOD__ );
1054  }
1055  }
1056 
1057  // TODO: MCR: determine $hasLinks for each slot, and use that info
1058  // with that slot's Content's isCountable method. That requires per-
1059  // slot ParserOutput in the ParserCache, or per-slot info in the
1060  // pagelinks table.
1061  return $content->isCountable( $hasLinks );
1062  }
1063 
1072  public function getRedirectTarget() {
1073  if ( $this->mRedirectTarget !== null ) {
1074  return $this->mRedirectTarget;
1075  }
1076 
1077  if ( $this->mHasRedirectTarget === false || !$this->getPageIsRedirectField() ) {
1078  return null;
1079  }
1080 
1081  // Query the redirect table
1082  $dbr = wfGetDB( DB_REPLICA );
1083  $row = $dbr->selectRow( 'redirect',
1084  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1085  [ 'rd_from' => $this->getId() ],
1086  __METHOD__
1087  );
1088 
1089  // rd_fragment and rd_interwiki were added later, populate them if empty
1090  if ( $row && $row->rd_fragment !== null && $row->rd_interwiki !== null ) {
1091  // (T203942) We can't redirect to Media namespace because it's virtual.
1092  // We don't want to modify Title objects farther down the
1093  // line. So, let's fix this here by changing to File namespace.
1094  if ( $row->rd_namespace == NS_MEDIA ) {
1095  $namespace = NS_FILE;
1096  } else {
1097  $namespace = $row->rd_namespace;
1098  }
1099  // T261347: be defensive when fetching data from the redirect table.
1100  // Use Title::makeTitleSafe(), and if that returns null, ignore the
1101  // row. In an ideal world, the DB would be cleaned up after a
1102  // namespace change, but nobody could be bothered to do that.
1103  $this->mRedirectTarget = Title::makeTitleSafe(
1104  $namespace, $row->rd_title,
1105  $row->rd_fragment, $row->rd_interwiki
1106  );
1107  $this->mHasRedirectTarget = $this->mRedirectTarget !== null;
1108  return $this->mRedirectTarget;
1109  }
1110 
1111  // This page doesn't have an entry in the redirect table
1112  $this->mRedirectTarget = $this->insertRedirect();
1113  $this->mHasRedirectTarget = $this->mRedirectTarget !== null;
1114  return $this->mRedirectTarget;
1115  }
1116 
1125  public function insertRedirect() {
1126  $content = $this->getContent();
1127  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1128  if ( !$retval ) {
1129  return null;
1130  }
1131 
1132  // Update the DB post-send if the page has not cached since now
1133  $latest = $this->getLatest();
1135  function () use ( $retval, $latest ) {
1136  $this->insertRedirectEntry( $retval, $latest );
1137  },
1138  DeferredUpdates::POSTSEND,
1139  wfGetDB( DB_PRIMARY )
1140  );
1141 
1142  return $retval;
1143  }
1144 
1151  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1152  if ( !$rt->isValidRedirectTarget() ) {
1153  // Don't put a bad redirect into the database (T278367)
1154  return false;
1155  }
1156 
1157  $dbw = wfGetDB( DB_PRIMARY );
1158  $dbw->startAtomic( __METHOD__ );
1159 
1160  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1161  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1162  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1163  $dbw->upsert(
1164  'redirect',
1165  [
1166  'rd_from' => $this->getId(),
1167  'rd_namespace' => $rt->getNamespace(),
1168  'rd_title' => $rt->getDBkey(),
1169  'rd_fragment' => $truncatedFragment,
1170  'rd_interwiki' => $rt->getInterwiki(),
1171  ],
1172  'rd_from',
1173  [
1174  'rd_namespace' => $rt->getNamespace(),
1175  'rd_title' => $rt->getDBkey(),
1176  'rd_fragment' => $truncatedFragment,
1177  'rd_interwiki' => $rt->getInterwiki(),
1178  ],
1179  __METHOD__
1180  );
1181  $success = true;
1182  } else {
1183  $success = false;
1184  }
1185 
1186  $dbw->endAtomic( __METHOD__ );
1187 
1188  return $success;
1189  }
1190 
1196  public function followRedirect() {
1197  return $this->getRedirectURL( $this->getRedirectTarget() );
1198  }
1199 
1207  public function getRedirectURL( $rt ) {
1208  if ( !$rt ) {
1209  return false;
1210  }
1211 
1212  if ( $rt->isExternal() ) {
1213  if ( $rt->isLocal() ) {
1214  // Offsite wikis need an HTTP redirect.
1215  // This can be hard to reverse and may produce loops,
1216  // so they may be disabled in the site configuration.
1217  $source = $this->mTitle->getFullURL( 'redirect=no' );
1218  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1219  } else {
1220  // External pages without "local" bit set are not valid
1221  // redirect targets
1222  return false;
1223  }
1224  }
1225 
1226  if ( $rt->isSpecialPage() ) {
1227  // Gotta handle redirects to special pages differently:
1228  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1229  // Some pages are not valid targets.
1230  if ( $rt->isValidRedirectTarget() ) {
1231  return $rt->getFullURL();
1232  } else {
1233  return false;
1234  }
1235  } elseif ( !$rt->isValidRedirectTarget() ) {
1236  // We somehow got a bad redirect target into the database (T278367)
1237  return false;
1238  }
1239 
1240  return $rt;
1241  }
1242 
1248  public function getContributors() {
1249  // @todo: This is expensive; cache this info somewhere.
1250 
1251  $dbr = wfGetDB( DB_REPLICA );
1252 
1253  $actorMigration = ActorMigration::newMigration();
1254  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1255 
1256  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1257 
1258  $revactor_actor = $actorQuery['fields']['rev_actor'];
1259  $fields = [
1260  'user_id' => $actorQuery['fields']['rev_user'],
1261  'user_name' => $actorQuery['fields']['rev_user_text'],
1262  'actor_id' => "MIN($revactor_actor)",
1263  'user_real_name' => 'MIN(user_real_name)',
1264  'timestamp' => 'MAX(rev_timestamp)',
1265  ];
1266 
1267  $conds = [ 'rev_page' => $this->getId() ];
1268 
1269  // The user who made the top revision gets credited as "this page was last edited by
1270  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1271  $user = $this->getUser()
1272  ? User::newFromId( $this->getUser() )
1273  : User::newFromName( $this->getUserText(), false );
1274  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1275 
1276  // Username hidden?
1277  $conds[] = "{$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER )} = 0";
1278 
1279  $jconds = [
1280  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1281  ] + $actorQuery['joins'];
1282 
1283  $options = [
1284  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1285  'ORDER BY' => 'timestamp DESC',
1286  ];
1287 
1288  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1289  return new UserArrayFromResult( $res );
1290  }
1291 
1299  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1300  // NOTE: Keep in sync with ParserOutputAccess::shouldUseCache().
1301  // TODO: Once ParserOutputAccess is stable, deprecated this method.
1302  return $this->exists()
1303  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1304  && $this->getContentHandler()->isParserCacheSupported();
1305  }
1306 
1322  public function getParserOutput(
1323  ParserOptions $parserOptions, $oldid = null, $noCache = false
1324  ) {
1325  if ( $oldid ) {
1326  $revision = $this->getRevisionStore()->getRevisionByTitle( $this->getTitle(), $oldid );
1327 
1328  if ( !$revision ) {
1329  return false;
1330  }
1331  } else {
1332  $revision = $this->getRevisionRecord();
1333  }
1334 
1335  $options = $noCache ? ParserOutputAccess::OPT_NO_CACHE : 0;
1336 
1337  $status = MediaWikiServices::getInstance()->getParserOutputAccess()->getParserOutput(
1338  $this, $parserOptions, $revision, $options
1339  );
1340  return $status->isOK() ? $status->getValue() : false; // convert null to false
1341  }
1342 
1348  public function doViewUpdates( Authority $performer, $oldid = 0 ) {
1349  if ( wfReadOnly() ) {
1350  return;
1351  }
1352 
1353  // Update newtalk / watchlist notification status;
1354  // Avoid outage if the master is not reachable by using a deferred updated
1356  function () use ( $performer, $oldid ) {
1357  $legacyUser = MediaWikiServices::getInstance()
1358  ->getUserFactory()
1359  ->newFromAuthority( $performer );
1360  $this->getHookRunner()->onPageViewUpdates( $this, $legacyUser );
1361 
1362  MediaWikiServices::getInstance()
1363  ->getWatchlistManager()
1364  ->clearTitleUserNotifications( $performer, $this, $oldid );
1365  },
1366  DeferredUpdates::PRESEND
1367  );
1368  }
1369 
1376  public function doPurge() {
1377  if ( !$this->getHookRunner()->onArticlePurge( $this ) ) {
1378  return false;
1379  }
1380 
1381  $this->mTitle->invalidateCache();
1382 
1383  // Clear file cache and send purge after above page_touched update was committed
1384  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1385  $hcu->purgeTitleUrls( $this->mTitle, $hcu::PURGE_PRESEND );
1386 
1387  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) {
1388  MediaWikiServices::getInstance()->getMessageCache()
1389  ->updateMessageOverride( $this->mTitle, $this->getContent() );
1390  }
1391 
1392  return true;
1393  }
1394 
1411  public function insertOn( $dbw, $pageId = null ) {
1412  $this->assertProperPage();
1413 
1414  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1415  $dbw->insert(
1416  'page',
1417  [
1418  'page_namespace' => $this->mTitle->getNamespace(),
1419  'page_title' => $this->mTitle->getDBkey(),
1420  'page_restrictions' => '',
1421  'page_is_redirect' => 0, // Will set this shortly...
1422  'page_is_new' => 1,
1423  'page_random' => wfRandom(),
1424  'page_touched' => $dbw->timestamp(),
1425  'page_latest' => 0, // Fill this in shortly...
1426  'page_len' => 0, // Fill this in shortly...
1427  ] + $pageIdForInsert,
1428  __METHOD__,
1429  [ 'IGNORE' ]
1430  );
1431 
1432  if ( $dbw->affectedRows() > 0 ) {
1433  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1434  $this->mId = $newid;
1435  $this->mTitle->resetArticleID( $newid );
1436 
1437  return $newid;
1438  } else {
1439  return false; // nothing changed
1440  }
1441  }
1442 
1458  public function updateRevisionOn(
1459  $dbw,
1460  RevisionRecord $revision,
1461  $lastRevision = null,
1462  $lastRevIsRedirect = null
1463  ) {
1464  // TODO: move into PageUpdater or PageStore
1465  // NOTE: when doing that, make sure cached fields get reset in doUserEditContent,
1466  // and in the compat stub!
1467 
1468  // Assertion to try to catch T92046
1469  if ( (int)$revision->getId() === 0 ) {
1470  throw new InvalidArgumentException(
1471  __METHOD__ . ': revision has ID ' . var_export( $revision->getId(), 1 )
1472  );
1473  }
1474 
1475  $content = $revision->getContent( SlotRecord::MAIN );
1476  $len = $content ? $content->getSize() : 0;
1477  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1478  $isNew = ( $lastRevision === 0 ) ? 1 : 0;
1479  $isRedirect = $rt !== null ? 1 : 0;
1480 
1481  $conditions = [ 'page_id' => $this->getId() ];
1482 
1483  if ( $lastRevision !== null ) {
1484  // An extra check against threads stepping on each other
1485  $conditions['page_latest'] = $lastRevision;
1486  }
1487 
1488  $revId = $revision->getId();
1489  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1490 
1491  $model = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel();
1492 
1493  $row = [ /* SET */
1494  'page_latest' => $revId,
1495  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1496  'page_is_new' => $isNew,
1497  'page_is_redirect' => $isRedirect,
1498  'page_len' => $len,
1499  'page_content_model' => $model,
1500  ];
1501 
1502  $dbw->update( 'page',
1503  $row,
1504  $conditions,
1505  __METHOD__ );
1506 
1507  $result = $dbw->affectedRows() > 0;
1508  if ( $result ) {
1509  $this->mTitle->loadFromRow( (object)$row );
1510  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1511  $this->setLastEdit( $revision );
1512  $this->mRedirectTarget = null;
1513  $this->mHasRedirectTarget = null;
1514  $this->mPageIsRedirectField = (bool)$rt;
1515  $this->mIsNew = (bool)$isNew;
1516  $this->mIsRedirect = (bool)$isRedirect;
1517  // Update the LinkCache.
1518  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1519  $linkCache->addGoodLinkObj(
1520  $this->getId(),
1521  $this->mTitle,
1522  $len,
1523  $this->mPageIsRedirectField,
1524  $this->mLatest,
1525  $model
1526  );
1527  }
1528 
1529  return $result;
1530  }
1531 
1543  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1544  // Always update redirects (target link might have changed)
1545  // Update/Insert if we don't know if the last revision was a redirect or not
1546  // Delete if changing from redirect to non-redirect
1547  $isRedirect = $redirectTitle !== null;
1548 
1549  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1550  return true;
1551  }
1552 
1553  if ( $isRedirect ) {
1554  $success = $this->insertRedirectEntry( $redirectTitle );
1555  } else {
1556  // This is not a redirect, remove row from redirect table
1557  $where = [ 'rd_from' => $this->getId() ];
1558  $dbw->delete( 'redirect', $where, __METHOD__ );
1559  $success = true;
1560  }
1561 
1562  if ( $this->getTitle()->getNamespace() === NS_FILE ) {
1563  MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
1564  ->invalidateImageRedirect( $this->getTitle() );
1565  }
1566 
1567  return $success;
1568  }
1569 
1583  $aSlots = $a->getSlots();
1584  $bSlots = $b->getSlots();
1585  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1586 
1587  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1588  }
1589 
1600  public function supportsSections() {
1601  return $this->getContentHandler()->supportsSections();
1602  }
1603 
1618  public function replaceSectionContent(
1619  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1620  ) {
1621  $baseRevId = null;
1622  if ( $edittime && $sectionId !== 'new' ) {
1623  $lb = $this->getDBLoadBalancer();
1624  $rev = $this->getRevisionStore()->getRevisionByTimestamp( $this->mTitle, $edittime );
1625  // Try the primary database if this thread may have just added it.
1626  // The logic to fallback to the primary database if the replica is missing
1627  // the revision could be generalized into RevisionStore, but we don't want
1628  // to encourage loading of revisions by timestamp.
1629  if ( !$rev
1630  && $lb->getServerCount() > 1
1631  && $lb->hasOrMadeRecentMasterChanges()
1632  ) {
1633  $rev = $this->getRevisionStore()->getRevisionByTimestamp(
1634  $this->mTitle, $edittime, RevisionStore::READ_LATEST );
1635  }
1636  if ( $rev ) {
1637  $baseRevId = $rev->getId();
1638  }
1639  }
1640 
1641  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1642  }
1643 
1657  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1658  $sectionTitle = '', $baseRevId = null
1659  ) {
1660  if ( strval( $sectionId ) === '' ) {
1661  // Whole-page edit; let the whole text through
1662  $newContent = $sectionContent;
1663  } else {
1664  if ( !$this->supportsSections() ) {
1665  throw new MWException( "sections not supported for content model " .
1666  $this->getContentHandler()->getModelID() );
1667  }
1668 
1669  // T32711: always use current version when adding a new section
1670  if ( $baseRevId === null || $sectionId === 'new' ) {
1671  $oldContent = $this->getContent();
1672  } else {
1673  $revRecord = $this->getRevisionStore()->getRevisionById( $baseRevId );
1674  if ( !$revRecord ) {
1675  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1676  $this->getId() . "; section: $sectionId)" );
1677  return null;
1678  }
1679 
1680  $oldContent = $revRecord->getContent( SlotRecord::MAIN );
1681  }
1682 
1683  if ( !$oldContent ) {
1684  wfDebug( __METHOD__ . ": no page text" );
1685  return null;
1686  }
1687 
1688  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1689  }
1690 
1691  return $newContent;
1692  }
1693 
1703  public function checkFlags( $flags ) {
1704  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1705  if ( $this->exists() ) {
1706  $flags |= EDIT_UPDATE;
1707  } else {
1708  $flags |= EDIT_NEW;
1709  }
1710  }
1711 
1712  return $flags;
1713  }
1714 
1742  private function getDerivedDataUpdater(
1743  UserIdentity $forUser = null,
1744  RevisionRecord $forRevision = null,
1745  RevisionSlotsUpdate $forUpdate = null,
1746  $forEdit = false
1747  ) {
1748  if ( !$forRevision && !$forUpdate ) {
1749  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1750  // going to use it with.
1751  $this->derivedDataUpdater = null;
1752  }
1753 
1754  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1755  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1756  // to it did not yet initialize it, because we don't know what data it will be
1757  // initialized with.
1758  $this->derivedDataUpdater = null;
1759  }
1760 
1761  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1762  // However, there is no good way to construct a cache key. We'd need to check against all
1763  // cached instances.
1764 
1765  if ( $this->derivedDataUpdater
1766  && !$this->derivedDataUpdater->isReusableFor(
1767  $forUser,
1768  $forRevision,
1769  $forUpdate,
1770  $forEdit ? $this->getLatest() : null
1771  )
1772  ) {
1773  $this->derivedDataUpdater = null;
1774  }
1775 
1776  if ( !$this->derivedDataUpdater ) {
1777  $this->derivedDataUpdater =
1778  $this->getPageUpdaterFactory()->newDerivedPageDataUpdater( $this );
1779  }
1780 
1782  }
1783 
1804  public function newPageUpdater( $performer, RevisionSlotsUpdate $forUpdate = null ) {
1805  if ( $performer instanceof Authority ) {
1806  // TODO: Deprecate this. But better get rid of this method entirely.
1807  $performer = $performer->getUser();
1808  }
1809 
1810  $pageUpdater = $this->getPageUpdaterFactory()->newPageUpdaterForDerivedPageDataUpdater(
1811  $this,
1812  $performer,
1813  $this->getDerivedDataUpdater( $performer, null, $forUpdate, true )
1814  );
1815 
1816  return $pageUpdater;
1817  }
1818 
1884  public function doEditContent(
1885  Content $content, $summary, $flags = 0, $originalRevId = false,
1886  Authority $performer = null, $serialFormat = null, $tags = [], $undidRevId = 0
1887  ) {
1888  wfDeprecated( __METHOD__, '1.32' );
1889 
1890  if ( !$performer ) {
1891  // Its okay to fallback to $wgUser because this whole method is deprecated
1892  global $wgUser;
1893  $performer = $wgUser;
1894  }
1895 
1896  return $this->doUserEditContent(
1897  $content, $performer, $summary, $flags, $originalRevId, $tags, $undidRevId
1898  );
1899  }
1900 
1961  public function doUserEditContent(
1962  Content $content,
1963  Authority $performer,
1964  $summary,
1965  $flags = 0,
1966  $originalRevId = false,
1967  $tags = [],
1968  $undidRevId = 0
1969  ) {
1971 
1972  if ( !( $summary instanceof CommentStoreComment ) ) {
1973  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1974  }
1975 
1976  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1977  // Checking the minoredit right should be done in the same place the 'bot' right is
1978  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1979  if ( ( $flags & EDIT_MINOR ) && !$performer->isAllowed( 'minoredit' ) ) {
1980  $flags &= ~EDIT_MINOR;
1981  }
1982 
1983  $slotsUpdate = new RevisionSlotsUpdate();
1984  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1985 
1986  // NOTE: while doUserEditContent() executes, callbacks to getDerivedDataUpdater and
1987  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1988  // used by this PageUpdater. However, there is no guarantee for this.
1989  $updater = $this->newPageUpdater( $performer, $slotsUpdate )
1990  ->setContent( SlotRecord::MAIN, $content );
1991 
1992  $revisionStore = $this->getRevisionStore();
1993  $originalRevision = $originalRevId ? $revisionStore->getRevisionById( $originalRevId ) : null;
1994  if ( $originalRevision && $undidRevId !== 0 ) {
1995  // Mark it as a revert if it's an undo
1996  $oldestRevertedRev = $revisionStore->getNextRevision( $originalRevision );
1997  if ( $oldestRevertedRev ) {
1998  $updater->markAsRevert(
1999  EditResult::REVERT_UNDO,
2000  $oldestRevertedRev->getId(),
2001  $undidRevId
2002  );
2003  } else {
2004  // We can't find the oldest reverted revision for some reason
2005  $updater->markAsRevert( EditResult::REVERT_UNDO, $undidRevId );
2006  }
2007  } elseif ( $undidRevId !== 0 ) {
2008  // It's an undo, but the original revision is not specified, fall back to just
2009  // marking it as an undo with one revision undone.
2010  $updater->markAsRevert( EditResult::REVERT_UNDO, $undidRevId );
2011  // Try finding the original revision ID by assuming it's the one before the edit
2012  // that is being undone. If the bet fails, $originalRevision is ignored anyway, so
2013  // no damage is done.
2014  $undidRevision = $revisionStore->getRevisionById( $undidRevId );
2015  if ( $undidRevision ) {
2016  $originalRevision = $revisionStore->getPreviousRevision( $undidRevision );
2017  }
2018  }
2019 
2020  // Make sure original revision's content is the same as the new content and save the
2021  // original revision ID.
2022  if ( $originalRevision &&
2023  $originalRevision->getContent( SlotRecord::MAIN )->equals( $content )
2024  ) {
2025  $updater->setOriginalRevisionId( $originalRevision->getId() );
2026  }
2027 
2028  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
2029 
2030  // TODO: this logic should not be in the storage layer, it's here for compatibility
2031  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
2032  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
2033 
2034  if ( $needsPatrol && $performer->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
2035  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
2036  }
2037 
2038  $updater->addTags( $tags );
2039 
2040  $revRec = $updater->saveRevision(
2041  $summary,
2042  $flags
2043  );
2044 
2045  // $revRec will be null if the edit failed, or if no new revision was created because
2046  // the content did not change.
2047  if ( $revRec ) {
2048  // update cached fields
2049  // TODO: this is currently redundant to what is done in updateRevisionOn.
2050  // But updateRevisionOn() should move into PageStore, and then this will be needed.
2051  $this->setLastEdit( $revRec );
2052  }
2053 
2054  return $updater->getStatus();
2055  }
2056 
2071  public function makeParserOptions( $context ) {
2072  $options = ParserOptions::newCanonical( $context );
2073 
2074  if ( $this->getTitle()->isConversionTable() ) {
2075  // @todo ConversionTable should become a separate content model, so
2076  // we don't need special cases like this one.
2077  $options->disableContentConversion();
2078  }
2079 
2080  return $options;
2081  }
2082 
2102  public function prepareContentForEdit(
2103  Content $content,
2104  RevisionRecord $revision = null,
2105  UserIdentity $user = null,
2106  $serialFormat = null,
2107  $useCache = true
2108  ) {
2109  if ( !$user ) {
2110  wfDeprecated( __METHOD__ . ' without a UserIdentity', '1.37' );
2111  global $wgUser;
2112  $user = $wgUser;
2113  }
2114 
2115  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2116  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2117 
2118  if ( !$updater->isUpdatePrepared() ) {
2119  $updater->prepareContent( $user, $slots, $useCache );
2120 
2121  if ( $revision ) {
2122  $updater->prepareUpdate(
2123  $revision,
2124  [
2125  'causeAction' => 'prepare-edit',
2126  'causeAgent' => $user->getName(),
2127  ]
2128  );
2129  }
2130  }
2131 
2132  return $updater->getPreparedEdit();
2133  }
2134 
2163  public function doEditUpdates(
2164  RevisionRecord $revisionRecord,
2165  UserIdentity $user,
2166  array $options = []
2167  ) {
2168  $options += [
2169  'causeAction' => 'edit-page',
2170  'causeAgent' => $user->getName(),
2171  ];
2172 
2173  $updater = $this->getDerivedDataUpdater( $user, $revisionRecord );
2174 
2175  $updater->prepareUpdate( $revisionRecord, $options );
2176 
2177  $updater->doUpdates();
2178  }
2179 
2193  public function updateParserCache( array $options = [] ) {
2194  $revision = $this->getRevisionRecord();
2195  if ( !$revision || !$revision->getId() ) {
2196  LoggerFactory::getInstance( 'wikipage' )->info(
2197  __METHOD__ . ' called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2198  );
2199  return;
2200  }
2201  $userIdentity = $revision->getUser( RevisionRecord::RAW );
2202 
2203  $updater = $this->getDerivedDataUpdater( $userIdentity, $revision );
2204  $updater->prepareUpdate( $revision, $options );
2205  $updater->doParserCacheUpdate();
2206  }
2207 
2237  public function doSecondaryDataUpdates( array $options = [] ) {
2238  $options['recursive'] = $options['recursive'] ?? true;
2239  $revision = $this->getRevisionRecord();
2240  if ( !$revision || !$revision->getId() ) {
2241  LoggerFactory::getInstance( 'wikipage' )->info(
2242  __METHOD__ . ' called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2243  );
2244  return;
2245  }
2246  $userIdentity = $revision->getUser( RevisionRecord::RAW );
2247 
2248  $updater = $this->getDerivedDataUpdater( $userIdentity, $revision );
2249  $updater->prepareUpdate( $revision, $options );
2250  $updater->doSecondaryDataUpdates( $options );
2251  }
2252 
2267  public function doUpdateRestrictions( array $limit, array $expiry,
2268  &$cascade, $reason, UserIdentity $user, $tags = null
2269  ) {
2271 
2272  $this->assertProperPage();
2273 
2274  if ( wfReadOnly() ) {
2275  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2276  }
2277 
2278  $this->loadPageData( 'fromdbmaster' );
2279  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2280  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2281  $id = $this->getId();
2282 
2283  if ( !$cascade ) {
2284  $cascade = false;
2285  }
2286 
2287  // Take this opportunity to purge out expired restrictions
2289 
2290  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2291  // we expect a single selection, but the schema allows otherwise.
2292  $isProtected = false;
2293  $protect = false;
2294  $changed = false;
2295 
2296  $dbw = wfGetDB( DB_PRIMARY );
2297 
2298  foreach ( $restrictionTypes as $action ) {
2299  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2300  $expiry[$action] = 'infinity';
2301  }
2302  if ( !isset( $limit[$action] ) ) {
2303  $limit[$action] = '';
2304  } elseif ( $limit[$action] != '' ) {
2305  $protect = true;
2306  }
2307 
2308  // Get current restrictions on $action
2309  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2310  if ( $current != '' ) {
2311  $isProtected = true;
2312  }
2313 
2314  if ( $limit[$action] != $current ) {
2315  $changed = true;
2316  } elseif ( $limit[$action] != '' ) {
2317  // Only check expiry change if the action is actually being
2318  // protected, since expiry does nothing on an not-protected
2319  // action.
2320  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2321  $changed = true;
2322  }
2323  }
2324  }
2325 
2326  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2327  $changed = true;
2328  }
2329 
2330  // If nothing has changed, do nothing
2331  if ( !$changed ) {
2332  return Status::newGood();
2333  }
2334 
2335  if ( !$protect ) { // No protection at all means unprotection
2336  $revCommentMsg = 'unprotectedarticle-comment';
2337  $logAction = 'unprotect';
2338  } elseif ( $isProtected ) {
2339  $revCommentMsg = 'modifiedarticleprotection-comment';
2340  $logAction = 'modify';
2341  } else {
2342  $revCommentMsg = 'protectedarticle-comment';
2343  $logAction = 'protect';
2344  }
2345 
2346  $logRelationsValues = [];
2347  $logRelationsField = null;
2348  $logParamsDetails = [];
2349 
2350  // Null revision (used for change tag insertion)
2351  $nullRevisionRecord = null;
2352 
2353  if ( $id ) { // Protection of existing page
2354  $legacyUser = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user );
2355  if ( !$this->getHookRunner()->onArticleProtect( $this, $legacyUser, $limit, $reason ) ) {
2356  return Status::newGood();
2357  }
2358 
2359  // Only certain restrictions can cascade...
2360  $editrestriction = isset( $limit['edit'] )
2361  ? [ $limit['edit'] ]
2362  : $this->mTitle->getRestrictions( 'edit' );
2363  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2364  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2365  }
2366  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2367  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2368  }
2369 
2370  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2371  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2372  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2373  }
2374  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2375  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2376  }
2377 
2378  // The schema allows multiple restrictions
2379  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2380  $cascade = false;
2381  }
2382 
2383  // insert null revision to identify the page protection change as edit summary
2384  $latest = $this->getLatest();
2385  $nullRevisionRecord = $this->insertNullProtectionRevision(
2386  $revCommentMsg,
2387  $limit,
2388  $expiry,
2389  $cascade,
2390  $reason,
2391  $user
2392  );
2393 
2394  if ( $nullRevisionRecord === null ) {
2395  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2396  }
2397 
2398  $logRelationsField = 'pr_id';
2399 
2400  // T214035: Avoid deadlock on MySQL.
2401  // Do a DELETE by primary key (pr_id) for any existing protection rows.
2402  // On MySQL and derivatives, unconditionally deleting by page ID (pr_page) would.
2403  // place a gap lock if there are no matching rows. This can deadlock when another
2404  // thread modifies protection settings for page IDs in the same gap.
2405  $existingProtectionIds = $dbw->selectFieldValues(
2406  'page_restrictions',
2407  'pr_id',
2408  [
2409  'pr_page' => $id,
2410  'pr_type' => array_map( 'strval', array_keys( $limit ) )
2411  ],
2412  __METHOD__
2413  );
2414 
2415  if ( $existingProtectionIds ) {
2416  $dbw->delete(
2417  'page_restrictions',
2418  [ 'pr_id' => $existingProtectionIds ],
2419  __METHOD__
2420  );
2421  }
2422 
2423  // Update restrictions table
2424  foreach ( $limit as $action => $restrictions ) {
2425  if ( $restrictions != '' ) {
2426  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2427  $dbw->insert(
2428  'page_restrictions',
2429  [
2430  'pr_page' => $id,
2431  'pr_type' => $action,
2432  'pr_level' => $restrictions,
2433  'pr_cascade' => $cascadeValue,
2434  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2435  ],
2436  __METHOD__
2437  );
2438  $logRelationsValues[] = $dbw->insertId();
2439  $logParamsDetails[] = [
2440  'type' => $action,
2441  'level' => $restrictions,
2442  'expiry' => $expiry[$action],
2443  'cascade' => (bool)$cascadeValue,
2444  ];
2445  }
2446  }
2447 
2448  // Clear out legacy restriction fields
2449  $dbw->update(
2450  'page',
2451  [ 'page_restrictions' => '' ],
2452  [ 'page_id' => $id ],
2453  __METHOD__
2454  );
2455 
2456  $this->getHookRunner()->onRevisionFromEditComplete(
2457  $this, $nullRevisionRecord, $latest, $user, $tags );
2458 
2459  $this->getHookRunner()->onArticleProtectComplete( $this, $legacyUser, $limit, $reason );
2460  } else { // Protection of non-existing page (also known as "title protection")
2461  // Cascade protection is meaningless in this case
2462  $cascade = false;
2463 
2464  if ( $limit['create'] != '' ) {
2465  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2466  $dbw->replace( 'protected_titles',
2467  [ [ 'pt_namespace', 'pt_title' ] ],
2468  [
2469  'pt_namespace' => $this->mTitle->getNamespace(),
2470  'pt_title' => $this->mTitle->getDBkey(),
2471  'pt_create_perm' => $limit['create'],
2472  'pt_timestamp' => $dbw->timestamp(),
2473  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2474  'pt_user' => $user->getId(),
2475  ] + $commentFields, __METHOD__
2476  );
2477  $logParamsDetails[] = [
2478  'type' => 'create',
2479  'level' => $limit['create'],
2480  'expiry' => $expiry['create'],
2481  ];
2482  } else {
2483  $dbw->delete( 'protected_titles',
2484  [
2485  'pt_namespace' => $this->mTitle->getNamespace(),
2486  'pt_title' => $this->mTitle->getDBkey()
2487  ], __METHOD__
2488  );
2489  }
2490  }
2491 
2492  $this->mTitle->flushRestrictions();
2493  InfoAction::invalidateCache( $this->mTitle );
2494 
2495  if ( $logAction == 'unprotect' ) {
2496  $params = [];
2497  } else {
2498  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2499  $params = [
2500  '4::description' => $protectDescriptionLog, // parameter for IRC
2501  '5:bool:cascade' => $cascade,
2502  'details' => $logParamsDetails, // parameter for localize and api
2503  ];
2504  }
2505 
2506  // Update the protection log
2507  $logEntry = new ManualLogEntry( 'protect', $logAction );
2508  $logEntry->setTarget( $this->mTitle );
2509  $logEntry->setComment( $reason );
2510  $logEntry->setPerformer( $user );
2511  $logEntry->setParameters( $params );
2512  if ( $nullRevisionRecord !== null ) {
2513  $logEntry->setAssociatedRevId( $nullRevisionRecord->getId() );
2514  }
2515  $logEntry->addTags( $tags );
2516  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2517  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2518  }
2519  $logId = $logEntry->insert();
2520  $logEntry->publish( $logId );
2521 
2522  return Status::newGood( $logId );
2523  }
2524 
2539  string $revCommentMsg,
2540  array $limit,
2541  array $expiry,
2542  bool $cascade,
2543  string $reason,
2544  UserIdentity $user
2545  ): ?RevisionRecord {
2546  $dbw = wfGetDB( DB_PRIMARY );
2547 
2548  // Prepare a null revision to be added to the history
2549  $editComment = wfMessage(
2550  $revCommentMsg,
2551  $this->mTitle->getPrefixedText(),
2552  $user->getName()
2553  )->inContentLanguage()->text();
2554  if ( $reason ) {
2555  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2556  }
2557  $protectDescription = $this->protectDescription( $limit, $expiry );
2558  if ( $protectDescription ) {
2559  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2560  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2561  ->inContentLanguage()->text();
2562  }
2563  if ( $cascade ) {
2564  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2565  $editComment .= wfMessage( 'brackets' )->params(
2566  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2567  )->inContentLanguage()->text();
2568  }
2569 
2570  $revStore = $this->getRevisionStore();
2571  $comment = CommentStoreComment::newUnsavedComment( $editComment );
2572  $nullRevRecord = $revStore->newNullRevision(
2573  $dbw,
2574  $this->getTitle(),
2575  $comment,
2576  true,
2577  $user
2578  );
2579 
2580  if ( $nullRevRecord ) {
2581  $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
2582 
2583  // Update page record and touch page
2584  $oldLatest = $inserted->getParentId();
2585 
2586  $this->updateRevisionOn( $dbw, $inserted, $oldLatest );
2587 
2588  return $inserted;
2589  } else {
2590  return null;
2591  }
2592  }
2593 
2598  protected function formatExpiry( $expiry ) {
2599  if ( $expiry != 'infinity' ) {
2600  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2601  return wfMessage(
2602  'protect-expiring',
2603  $contLang->timeanddate( $expiry, false, false ),
2604  $contLang->date( $expiry, false, false ),
2605  $contLang->time( $expiry, false, false )
2606  )->inContentLanguage()->text();
2607  } else {
2608  return wfMessage( 'protect-expiry-indefinite' )
2609  ->inContentLanguage()->text();
2610  }
2611  }
2612 
2620  public function protectDescription( array $limit, array $expiry ) {
2621  $protectDescription = '';
2622 
2623  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2624  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2625  # All possible message keys are listed here for easier grepping:
2626  # * restriction-create
2627  # * restriction-edit
2628  # * restriction-move
2629  # * restriction-upload
2630  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2631  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2632  # with '' filtered out. All possible message keys are listed below:
2633  # * protect-level-autoconfirmed
2634  # * protect-level-sysop
2635  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2636  ->inContentLanguage()->text();
2637 
2638  $expiryText = $this->formatExpiry( $expiry[$action] );
2639 
2640  if ( $protectDescription !== '' ) {
2641  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2642  }
2643  $protectDescription .= wfMessage( 'protect-summary-desc' )
2644  ->params( $actionText, $restrictionsText, $expiryText )
2645  ->inContentLanguage()->text();
2646  }
2647 
2648  return $protectDescription;
2649  }
2650 
2662  public function protectDescriptionLog( array $limit, array $expiry ) {
2663  $protectDescriptionLog = '';
2664 
2665  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2666  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2667  $expiryText = $this->formatExpiry( $expiry[$action] );
2668  $protectDescriptionLog .=
2669  $dirMark .
2670  "[$action=$restrictions] ($expiryText)";
2671  }
2672 
2673  return trim( $protectDescriptionLog );
2674  }
2675 
2688  public function isBatchedDelete( $safetyMargin = 0 ) {
2690 
2691  $dbr = wfGetDB( DB_REPLICA );
2692  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2693  $revCount += $safetyMargin;
2694 
2695  return $revCount >= $wgDeleteRevisionsBatchSize;
2696  }
2697 
2723  public function doDeleteArticleReal(
2724  $reason, UserIdentity $deleter, $suppress = false, $u1 = null, &$error = '', $u2 = null,
2725  $tags = [], $logsubtype = 'delete', $immediate = false
2726  ) {
2727  wfDebug( __METHOD__ );
2728  $this->assertProperPage();
2729 
2730  $status = Status::newGood();
2731 
2732  $legacyDeleter = MediaWikiServices::getInstance()
2733  ->getUserFactory()
2734  ->newFromUserIdentity( $deleter );
2735  if ( !$this->getHookRunner()->onArticleDelete(
2736  $this, $legacyDeleter, $reason, $error, $status, $suppress )
2737  ) {
2738  if ( $status->isOK() ) {
2739  // Hook aborted but didn't set a fatal status
2740  $status->fatal( 'delete-hook-aborted' );
2741  }
2742  return $status;
2743  }
2744 
2745  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2746  $logsubtype, $immediate );
2747  }
2748 
2765  public function doDeleteArticleBatched(
2766  $reason, $suppress, UserIdentity $deleter, $tags,
2767  $logsubtype, $immediate = false, $webRequestId = null
2768  ) {
2769  wfDebug( __METHOD__ );
2770 
2771  $status = Status::newGood();
2772 
2773  $dbw = wfGetDB( DB_PRIMARY );
2774  $dbw->startAtomic( __METHOD__ );
2775 
2776  $this->loadPageData( self::READ_LATEST );
2777  $id = $this->getId();
2778  // T98706: lock the page from various other updates but avoid using
2779  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2780  // the revisions queries (which also JOIN on user). Only lock the page
2781  // row and CAS check on page_latest to see if the trx snapshot matches.
2782  $lockedLatest = $this->lockAndGetLatest();
2783  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2784  $dbw->endAtomic( __METHOD__ );
2785  // Page not there or trx snapshot is stale
2786  $status->error( 'cannotdelete',
2787  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2788  return $status;
2789  }
2790 
2791  // At this point we are now committed to returning an OK
2792  // status unless some DB query error or other exception comes up.
2793  // This way callers don't have to call rollback() if $status is bad
2794  // unless they actually try to catch exceptions (which is rare).
2795 
2796  // we need to remember the old content so we can use it to generate all deletion updates.
2797  $revisionRecord = $this->getRevisionRecord();
2798  try {
2799  $content = $this->getContent( RevisionRecord::RAW );
2800  } catch ( Exception $ex ) {
2801  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2802  . $ex->getMessage() );
2803 
2804  $content = null;
2805  }
2806 
2807  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2808  // one batch of revisions and defer archival of any others to the job queue.
2809  $explictTrxLogged = false;
2810  while ( true ) {
2811  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2812  if ( $done || !$immediate ) {
2813  break;
2814  }
2815  $dbw->endAtomic( __METHOD__ );
2816  if ( $dbw->explicitTrxActive() ) {
2817  // Explict transactions may never happen here in practice. Log to be sure.
2818  if ( !$explictTrxLogged ) {
2819  $explictTrxLogged = true;
2820  LoggerFactory::getInstance( 'wfDebug' )->debug(
2821  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2822  'title' => $this->getTitle()->getText(),
2823  ] );
2824  }
2825  continue;
2826  }
2827  if ( $dbw->trxLevel() ) {
2828  $dbw->commit( __METHOD__ );
2829  }
2830  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2831  $lbFactory->waitForReplication();
2832  $dbw->startAtomic( __METHOD__ );
2833  }
2834 
2835  // If done archiving, also delete the article.
2836  if ( !$done ) {
2837  $dbw->endAtomic( __METHOD__ );
2838 
2839  $jobParams = [
2840  'namespace' => $this->getTitle()->getNamespace(),
2841  'title' => $this->getTitle()->getDBkey(),
2842  'wikiPageId' => $id,
2843  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2844  'reason' => $reason,
2845  'suppress' => $suppress,
2846  'userId' => $deleter->getId(),
2847  'tags' => json_encode( $tags ),
2848  'logsubtype' => $logsubtype,
2849  ];
2850 
2851  $job = new DeletePageJob( $jobParams );
2852  JobQueueGroup::singleton()->push( $job );
2853 
2854  $status->warning( 'delete-scheduled',
2855  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2856  } else {
2857  // Get archivedRevisionCount by db query, because there's no better alternative.
2858  // Jobs cannot pass a count of archived revisions to the next job, because additional
2859  // deletion operations can be started while the first is running. Jobs from each
2860  // gracefully interleave, but would not know about each other's count. Deduplication
2861  // in the job queue to avoid simultaneous deletion operations would add overhead.
2862  // Number of archived revisions cannot be known beforehand, because edits can be made
2863  // while deletion operations are being processed, changing the number of archivals.
2864  $archivedRevisionCount = (int)$dbw->selectField(
2865  'archive', 'COUNT(*)',
2866  [
2867  'ar_namespace' => $this->getTitle()->getNamespace(),
2868  'ar_title' => $this->getTitle()->getDBkey(),
2869  'ar_page_id' => $id
2870  ], __METHOD__
2871  );
2872 
2873  // Clone the title and wikiPage, so we have the information we need when
2874  // we log and run the ArticleDeleteComplete hook.
2875  $logTitle = clone $this->mTitle;
2876  $wikiPageBeforeDelete = clone $this;
2877 
2878  // Now that it's safely backed up, delete it
2879  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2880 
2881  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2882  $logtype = $suppress ? 'suppress' : 'delete';
2883 
2884  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2885  $logEntry->setPerformer( $deleter );
2886  $logEntry->setTarget( $logTitle );
2887  $logEntry->setComment( $reason );
2888  $logEntry->addTags( $tags );
2889  $logid = $logEntry->insert();
2890 
2891  $dbw->onTransactionPreCommitOrIdle(
2892  static function () use ( $logEntry, $logid ) {
2893  // T58776: avoid deadlocks (especially from FileDeleteForm)
2894  $logEntry->publish( $logid );
2895  },
2896  __METHOD__
2897  );
2898 
2899  $dbw->endAtomic( __METHOD__ );
2900 
2901  $this->doDeleteUpdates(
2902  $id,
2903  $content,
2904  $revisionRecord,
2905  $deleter
2906  );
2907 
2908  $legacyDeleter = MediaWikiServices::getInstance()
2909  ->getUserFactory()
2910  ->newFromUserIdentity( $deleter );
2911  $this->getHookRunner()->onArticleDeleteComplete(
2912  $wikiPageBeforeDelete,
2913  $legacyDeleter,
2914  $reason,
2915  $id,
2916  $content,
2917  $logEntry,
2918  $archivedRevisionCount
2919  );
2920  $status->value = $logid;
2921 
2922  // Show log excerpt on 404 pages rather than just a link
2923  $dbCache = ObjectCache::getInstance( 'db-replicated' );
2924  $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2925  $dbCache->set( $key, 1, $dbCache::TTL_DAY );
2926  }
2927 
2928  return $status;
2929  }
2930 
2940  protected function archiveRevisions( $dbw, $id, $suppress ) {
2942 
2943  // Given the lock above, we can be confident in the title and page ID values
2944  $namespace = $this->getTitle()->getNamespace();
2945  $dbKey = $this->getTitle()->getDBkey();
2946 
2947  $commentStore = CommentStore::getStore();
2948 
2949  $revQuery = $this->getRevisionStore()->getQueryInfo();
2950  $bitfield = false;
2951 
2952  // Bitfields to further suppress the content
2953  if ( $suppress ) {
2954  $bitfield = RevisionRecord::SUPPRESSED_ALL;
2955  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2956  }
2957 
2958  // For now, shunt the revision data into the archive table.
2959  // Text is *not* removed from the text table; bulk storage
2960  // is left intact to avoid breaking block-compression or
2961  // immutable storage schemes.
2962  // In the future, we may keep revisions and mark them with
2963  // the rev_deleted field, which is reserved for this purpose.
2964 
2965  // Lock rows in `revision` and its temp tables, but not any others.
2966  // Note array_intersect() preserves keys from the first arg, and we're
2967  // assuming $revQuery has `revision` primary and isn't using subtables
2968  // for anything we care about.
2969  $dbw->lockForUpdate(
2970  array_intersect(
2971  $revQuery['tables'],
2972  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2973  ),
2974  [ 'rev_page' => $id ],
2975  __METHOD__,
2976  [],
2977  $revQuery['joins']
2978  );
2979 
2980  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2981  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2982  $res = $dbw->select(
2983  $revQuery['tables'],
2984  $revQuery['fields'],
2985  [ 'rev_page' => $id ],
2986  __METHOD__,
2987  [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2988  $revQuery['joins']
2989  );
2990 
2991  // Build their equivalent archive rows
2992  $rowsInsert = [];
2993  $revids = [];
2994 
2996  $ipRevIds = [];
2997 
2998  $done = true;
2999  foreach ( $res as $row ) {
3000  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
3001  $done = false;
3002  break;
3003  }
3004 
3005  $comment = $commentStore->getComment( 'rev_comment', $row );
3006  $rowInsert = [
3007  'ar_namespace' => $namespace,
3008  'ar_title' => $dbKey,
3009  'ar_actor' => $row->rev_actor,
3010  'ar_timestamp' => $row->rev_timestamp,
3011  'ar_minor_edit' => $row->rev_minor_edit,
3012  'ar_rev_id' => $row->rev_id,
3013  'ar_parent_id' => $row->rev_parent_id,
3014  'ar_len' => $row->rev_len,
3015  'ar_page_id' => $id,
3016  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
3017  'ar_sha1' => $row->rev_sha1,
3018  ] + $commentStore->insert( $dbw, 'ar_comment', $comment );
3019 
3020  $rowsInsert[] = $rowInsert;
3021  $revids[] = $row->rev_id;
3022 
3023  // Keep track of IP edits, so that the corresponding rows can
3024  // be deleted in the ip_changes table.
3025  if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
3026  $ipRevIds[] = $row->rev_id;
3027  }
3028  }
3029 
3030  // This conditional is just a sanity check
3031  if ( count( $revids ) > 0 ) {
3032  // Copy them into the archive table
3033  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
3034 
3035  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
3036  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
3038  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
3039  }
3040 
3041  // Also delete records from ip_changes as applicable.
3042  if ( count( $ipRevIds ) > 0 ) {
3043  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
3044  }
3045  }
3046 
3047  return $done;
3048  }
3049 
3056  public function lockAndGetLatest() {
3057  return (int)wfGetDB( DB_PRIMARY )->selectField(
3058  'page',
3059  'page_latest',
3060  [
3061  'page_id' => $this->getId(),
3062  // Typically page_id is enough, but some code might try to do
3063  // updates assuming the title is the same, so verify that
3064  'page_namespace' => $this->getTitle()->getNamespace(),
3065  'page_title' => $this->getTitle()->getDBkey()
3066  ],
3067  __METHOD__,
3068  [ 'FOR UPDATE' ]
3069  );
3070  }
3071 
3084  public function doDeleteUpdates(
3085  $id,
3086  Content $content = null,
3087  RevisionRecord $revRecord = null,
3088  UserIdentity $user = null
3089  ) {
3090  if ( $id !== $this->getId() ) {
3091  throw new InvalidArgumentException( 'Mismatching page ID' );
3092  }
3093 
3094  try {
3095  $countable = $this->isCountable();
3096  } catch ( Exception $ex ) {
3097  // fallback for deleting broken pages for which we cannot load the content for
3098  // some reason. Note that doDeleteArticleReal() already logged this problem.
3099  $countable = false;
3100  }
3101 
3102  // Update site status
3104  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3105  ) );
3106 
3107  // Delete pagelinks, update secondary indexes, etc
3108  $updates = $this->getDeletionUpdates( $revRecord ?: $content );
3109  foreach ( $updates as $update ) {
3110  DeferredUpdates::addUpdate( $update );
3111  }
3112 
3113  $causeAgent = $user ? $user->getName() : 'unknown';
3114  // Reparse any pages transcluding this page
3116  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3117  // Reparse any pages including this image
3118  if ( $this->mTitle->getNamespace() === NS_FILE ) {
3120  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3121  }
3122 
3123  // Clear caches
3124  self::onArticleDelete( $this->mTitle );
3125 
3127  $this->mTitle,
3128  $revRecord,
3129  null,
3131  );
3132 
3133  // Reset this object and the Title object
3134  $this->loadFromRow( false, self::READ_LATEST );
3135 
3136  // Search engine
3137  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3138  }
3139 
3151  public static function onArticleCreate( Title $title ) {
3152  // TODO: move this into a PageEventEmitter service
3153 
3154  // Update existence markers on article/talk tabs...
3155  $other = $title->getOtherPage();
3156 
3157  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3158  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3159 
3160  $title->touchLinks();
3161  $title->deleteTitleProtection();
3162 
3163  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3164 
3165  // Invalidate caches of articles which include this page
3167  $title,
3168  'templatelinks',
3169  [ 'causeAction' => 'page-create' ]
3170  );
3171  JobQueueGroup::singleton()->lazyPush( $job );
3172 
3173  if ( $title->getNamespace() === NS_CATEGORY ) {
3174  // Load the Category object, which will schedule a job to create
3175  // the category table row if necessary. Checking a replica DB is ok
3176  // here, in the worst case it'll run an unnecessary recount job on
3177  // a category that probably doesn't have many members.
3178  Category::newFromTitle( $title )->getID();
3179  }
3180  }
3181 
3187  public static function onArticleDelete( Title $title ) {
3188  // TODO: move this into a PageEventEmitter service
3189 
3190  // Update existence markers on article/talk tabs...
3191  $other = $title->getOtherPage();
3192 
3193  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3194  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3195 
3196  $title->touchLinks();
3197 
3198  $services = MediaWikiServices::getInstance();
3199  $services->getLinkCache()->invalidateTitle( $title );
3200 
3202 
3203  // Messages
3204  if ( $title->getNamespace() === NS_MEDIAWIKI ) {
3205  $services->getMessageCache()->updateMessageOverride( $title, null );
3206  }
3207 
3208  // Images
3209  if ( $title->getNamespace() === NS_FILE ) {
3211  $title,
3212  'imagelinks',
3213  [ 'causeAction' => 'page-delete' ]
3214  );
3215  JobQueueGroup::singleton()->lazyPush( $job );
3216  }
3217 
3218  // User talk pages
3219  if ( $title->getNamespace() === NS_USER_TALK ) {
3220  $user = User::newFromName( $title->getText(), false );
3221  if ( $user ) {
3222  MediaWikiServices::getInstance()
3223  ->getTalkPageNotificationManager()
3224  ->removeUserHasNewMessages( $user );
3225  }
3226  }
3227 
3228  // Image redirects
3229  $services->getRepoGroup()->getLocalRepo()->invalidateImageRedirect( $title );
3230 
3231  // Purge cross-wiki cache entities referencing this page
3233  }
3234 
3243  public static function onArticleEdit(
3244  Title $title,
3245  RevisionRecord $revRecord = null,
3246  $slotsChanged = null
3247  ) {
3248  // TODO: move this into a PageEventEmitter service
3249 
3250  $jobs = [];
3251  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3252  // Invalidate caches of articles which include this page.
3253  // Only for the main slot, because only the main slot is transcluded.
3254  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3256  $title,
3257  'templatelinks',
3258  [ 'causeAction' => 'page-edit' ]
3259  );
3260  }
3261  // Invalidate the caches of all pages which redirect here
3263  $title,
3264  'redirect',
3265  [ 'causeAction' => 'page-edit' ]
3266  );
3267  JobQueueGroup::singleton()->lazyPush( $jobs );
3268 
3269  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3270 
3271  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3272  $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3273 
3274  // Purge ?action=info cache
3275  $revid = $revRecord ? $revRecord->getId() : null;
3276  DeferredUpdates::addCallableUpdate( static function () use ( $title, $revid ) {
3278  } );
3279 
3280  // Purge cross-wiki cache entities referencing this page
3282  }
3283 
3291  private static function purgeInterwikiCheckKey( Title $title ) {
3293 
3294  if ( !$wgEnableScaryTranscluding ) {
3295  return; // @todo: perhaps this wiki is only used as a *source* for content?
3296  }
3297 
3298  DeferredUpdates::addCallableUpdate( static function () use ( $title ) {
3299  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3300  $cache->resetCheckKey(
3301  // Do not include the namespace since there can be multiple aliases to it
3302  // due to different namespace text definitions on different wikis. This only
3303  // means that some cache invalidations happen that are not strictly needed.
3304  $cache->makeGlobalKey(
3305  'interwiki-page',
3307  $title->getDBkey()
3308  )
3309  );
3310  } );
3311  }
3312 
3319  public function getCategories() {
3320  $id = $this->getId();
3321  if ( $id == 0 ) {
3322  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3323  }
3324 
3325  $dbr = wfGetDB( DB_REPLICA );
3326  $res = $dbr->select( 'categorylinks',
3327  [ 'page_title' => 'cl_to', 'page_namespace' => NS_CATEGORY ],
3328  [ 'cl_from' => $id ],
3329  __METHOD__
3330  );
3331 
3332  return TitleArray::newFromResult( $res );
3333  }
3334 
3341  public function getHiddenCategories() {
3342  $result = [];
3343  $id = $this->getId();
3344 
3345  if ( $id == 0 ) {
3346  return [];
3347  }
3348 
3349  $dbr = wfGetDB( DB_REPLICA );
3350  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3351  [ 'cl_to' ],
3352  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3353  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3354  __METHOD__ );
3355 
3356  if ( $res !== false ) {
3357  foreach ( $res as $row ) {
3358  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3359  }
3360  }
3361 
3362  return $result;
3363  }
3364 
3372  public function getAutoDeleteReason( &$hasHistory ) {
3373  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3374  }
3375 
3386  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3387  $id = $id ?: $this->getId();
3388  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3389  getCategoryLinkType( $this->getTitle()->getNamespace() );
3390 
3391  $addFields = [ 'cat_pages = cat_pages + 1' ];
3392  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3393  if ( $type !== 'page' ) {
3394  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3395  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3396  }
3397 
3398  $dbw = wfGetDB( DB_PRIMARY );
3399 
3400  if ( count( $added ) ) {
3401  $existingAdded = $dbw->selectFieldValues(
3402  'category',
3403  'cat_title',
3404  [ 'cat_title' => $added ],
3405  __METHOD__
3406  );
3407 
3408  // For category rows that already exist, do a plain
3409  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3410  // to avoid creating gaps in the cat_id sequence.
3411  if ( count( $existingAdded ) ) {
3412  $dbw->update(
3413  'category',
3414  $addFields,
3415  [ 'cat_title' => $existingAdded ],
3416  __METHOD__
3417  );
3418  }
3419 
3420  $missingAdded = array_diff( $added, $existingAdded );
3421  if ( count( $missingAdded ) ) {
3422  $insertRows = [];
3423  foreach ( $missingAdded as $cat ) {
3424  $insertRows[] = [
3425  'cat_title' => $cat,
3426  'cat_pages' => 1,
3427  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3428  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3429  ];
3430  }
3431  $dbw->upsert(
3432  'category',
3433  $insertRows,
3434  'cat_title',
3435  $addFields,
3436  __METHOD__
3437  );
3438  }
3439  }
3440 
3441  if ( count( $deleted ) ) {
3442  $dbw->update(
3443  'category',
3444  $removeFields,
3445  [ 'cat_title' => $deleted ],
3446  __METHOD__
3447  );
3448  }
3449 
3450  foreach ( $added as $catName ) {
3451  $cat = Category::newFromName( $catName );
3452  $this->getHookRunner()->onCategoryAfterPageAdded( $cat, $this );
3453  }
3454 
3455  foreach ( $deleted as $catName ) {
3456  $cat = Category::newFromName( $catName );
3457  $this->getHookRunner()->onCategoryAfterPageRemoved( $cat, $this, $id );
3458  // Refresh counts on categories that should be empty now (after commit, T166757)
3459  DeferredUpdates::addCallableUpdate( static function () use ( $cat ) {
3460  $cat->refreshCountsIfEmpty();
3461  } );
3462  }
3463  }
3464 
3477  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3478  if ( wfReadOnly() ) {
3479  return;
3480  }
3481 
3482  if ( !$this->getHookRunner()->onOpportunisticLinksUpdate( $this,
3483  $this->mTitle, $parserOutput )
3484  ) {
3485  return;
3486  }
3487 
3488  $config = RequestContext::getMain()->getConfig();
3489 
3490  $params = [
3491  'isOpportunistic' => true,
3492  'rootJobTimestamp' => $parserOutput->getCacheTime()
3493  ];
3494 
3495  if ( $this->mTitle->areRestrictionsCascading() ) {
3496  // In general, MediaWiki does not re-run LinkUpdate (e.g. for search index, category
3497  // listings, and backlinks for Whatlinkshere), unless either the page was directly
3498  // edited, or was re-generate following a template edit propagating to an affected
3499  // page. As such, during page views when there is no valid ParserCache entry,
3500  // we re-parse and save, but leave indexes as-is.
3501  //
3502  // We make an exception for pages that have cascading protection (perhaps for a wiki's
3503  // "Main Page"). When such page is re-parsed on-demand after a parser cache miss, we
3504  // queue a high-priority LinksUpdate job, to ensure that we really protect all
3505  // content that is currently transcluded onto the page. This is important, because
3506  // wikitext supports conditional statements based on the current time, which enables
3507  // transcluding of a different sub page based on which day it is, and then show that
3508  // information on the Main Page, without the Main Page itself being edited.
3509  JobQueueGroup::singleton()->lazyPush(
3510  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3511  );
3512  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasReducedExpiry() ) {
3513  // Assume the output contains "dynamic" time/random based magic words.
3514  // Only update pages that expired due to dynamic content and NOT due to edits
3515  // to referenced templates/files. When the cache expires due to dynamic content,
3516  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3517  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3518  // template/file edit already triggered recursive RefreshLinksJob jobs.
3519  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3520  // If a page is uncacheable, do not keep spamming a job for it.
3521  // Although it would be de-duplicated, it would still waste I/O.
3523  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3524  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3525  if ( $cache->add( $key, time(), $ttl ) ) {
3526  JobQueueGroup::singleton()->lazyPush(
3527  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3528  );
3529  }
3530  }
3531  }
3532  }
3533 
3543  public function getDeletionUpdates( $rev = null ) {
3544  if ( !$rev ) {
3545  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3546 
3547  try {
3548  $rev = $this->getRevisionRecord();
3549  } catch ( Exception $ex ) {
3550  // If we can't load the content, something is wrong. Perhaps that's why
3551  // the user is trying to delete the page, so let's not fail in that case.
3552  // Note that doDeleteArticleReal() will already have logged an issue with
3553  // loading the content.
3554  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3555  }
3556  }
3557 
3558  if ( !$rev ) {
3559  $slotContent = [];
3560  } elseif ( $rev instanceof Content ) {
3561  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3562 
3563  $slotContent = [ SlotRecord::MAIN => $rev ];
3564  } else {
3565  $slotContent = array_map( static function ( SlotRecord $slot ) {
3566  return $slot->getContent();
3567  }, $rev->getSlots()->getSlots() );
3568  }
3569 
3570  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3571 
3572  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3573  // model here, not the content object!
3574  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3576  $content = null; // in case $slotContent is zero-length
3577  foreach ( $slotContent as $role => $content ) {
3578  $handler = $content->getContentHandler();
3579 
3580  $updates = $handler->getDeletionUpdates(
3581  $this->getTitle(),
3582  $role
3583  );
3584 
3585  $allUpdates = array_merge( $allUpdates, $updates );
3586  }
3587 
3588  $this->getHookRunner()->onPageDeletionDataUpdates(
3589  $this->getTitle(), $rev, $allUpdates );
3590 
3591  // TODO: hard deprecate old hook in 1.33
3592  $this->getHookRunner()->onWikiPageDeletionUpdates( $this, $content, $allUpdates );
3593  return $allUpdates;
3594  }
3595 
3603  public function isLocal() {
3604  return true;
3605  }
3606 
3616  public function getWikiDisplayName() {
3617  global $wgSitename;
3618  return $wgSitename;
3619  }
3620 
3629  public function getSourceURL() {
3630  return $this->getTitle()->getCanonicalURL();
3631  }
3632 
3639  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3640 
3641  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3642  }
3643 
3650  public function __wakeup() {
3651  // Make sure we re-fetch the latest state from the database.
3652  // In particular, the latest revision may have changed.
3653  // As a side-effect, this makes sure mLastRevision doesn't
3654  // end up being an instance of the old Revision class (see T259181),
3655  // especially since that class was removed entirely in 1.37.
3656  $this->clear();
3657  }
3658 
3663  public function getNamespace(): int {
3664  return $this->getTitle()->getNamespace();
3665  }
3666 
3671  public function getDBkey(): string {
3672  return $this->getTitle()->getDBkey();
3673  }
3674 
3679  public function getWikiId() {
3680  return $this->getTitle()->getWikiId();
3681  }
3682 
3687  public function canExist(): bool {
3688  // NOTE: once WikiPage becomes a ProperPageIdentity, this should always return true
3689  return $this->mTitle->canExist();
3690  }
3691 
3696  public function __toString(): string {
3697  return $this->mTitle->__toString();
3698  }
3699 
3707  public function isSamePageAs( PageReference $other ): bool {
3708  // NOTE: keep in sync with PageIdentityValue::isSamePageAs()!
3709 
3710  if ( $other->getWikiId() !== $this->getWikiId() ) {
3711  return false;
3712  }
3713 
3714  if ( $other->getNamespace() !== $this->getNamespace()
3715  || $other->getDBkey() !== $this->getDBkey() ) {
3716  return false;
3717  }
3718 
3719  return true;
3720  }
3721 
3733  public function toPageRecord(): ExistingPageRecord {
3734  // TODO: replace individual member fields with a PageRecord instance that is always present
3735 
3736  if ( !$this->mDataLoaded ) {
3737  $this->loadPageData();
3738  }
3739 
3740  Assert::precondition(
3741  $this->exists(),
3742  'This WikiPage instance does not represent an existing page: ' . $this->mTitle
3743  );
3744 
3745  return new PageStoreRecord(
3746  (object)[
3747  'page_id' => $this->getId(),
3748  'page_namespace' => $this->mTitle->getNamespace(),
3749  'page_title' => $this->mTitle->getDBkey(),
3750  'page_latest' => $this->mLatest,
3751  'page_is_new' => $this->mIsNew,
3752  'page_is_redirect' => $this->mIsRedirect,
3753  'page_touched' => $this->getTouched(),
3754  'page_lang' => $this->getLanguage()
3755  ],
3756  PageIdentity::LOCAL
3757  );
3758  }
3759 
3760 }
WikiPage\getCategories
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:3319
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:675
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:3733
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:28
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:643
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:1961
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:3151
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:472
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
WikiPage\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3372
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:865
ParserOutput
Definition: ParserOutput.php:31
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:1072
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:272
WikiPage\getComment
getComment( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:986
WikiPage\getWikiId
getWikiId()
Definition: WikiPage.php:3679
WikiPage\clearCacheFields
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:336
Title\getFragment
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1815
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:2688
WikiPage\wasLoadedFrom
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:529
TitleArray\newFromResult
static newFromResult( $res)
Definition: TitleArray.php:42
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1322
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:186
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:648
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:755
WikiPage\__toString
__toString()
Returns an informative human readable unique representation of the page identity, for use as a cache ...
Definition: WikiPage.php:3696
WikiPage\getUserText
getUserText( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:965
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1657
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1703
WikiPage\getLanguage
getLanguage()
Definition: WikiPage.php:765
SearchUpdate
Database independent search index updater.
Definition: SearchUpdate.php:36
WikiPage\$mDataLoadedFrom
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:134
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
Definition: DeferredUpdates.php:119
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:60
WikiPage\replaceSectionContent
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1618
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2071
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1207
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1099
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:602
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1182
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:1248
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1056
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:2163
WikiPage\doViewUpdates
doViewUpdates(Authority $performer, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1348
$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:1346
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\archiveRevisions
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:2940
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:2102
WikiPage\getDBLoadBalancer
getDBLoadBalancer()
Definition: WikiPage.php:285
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:295
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:8033
$revQuery
$revQuery
Definition: testCompression.php:56
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:8049
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:3477
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:2620
Title\castFromPageIdentity
static castFromPageIdentity(?PageIdentity $pageIdentity)
Return a Title for a given PageIdentity.
Definition: Title.php:364
WikiPage\$mLastRevision
RevisionRecord $mLastRevision
Definition: WikiPage.php:139
$dbr
$dbr
Definition: testCompression.php:54
WikiPage\updateParserCache
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2193
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1600
$wgEnableScaryTranscluding
$wgEnableScaryTranscluding
Enable interwiki transcluding.
Definition: DefaultSettings.php:5066
WikiPage\doDeleteArticleBatched
doDeleteArticleBatched( $reason, $suppress, UserIdentity $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2765
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:1088
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1028
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:1458
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(PageIdentity $page, ?RevisionRecord $old, ?RevisionRecord $new, string $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:560
WikiPage\isSamePageAs
isSamePageAs(PageReference $other)
Checks whether the given PageReference refers to the same page as this PageReference....
Definition: WikiPage.php:3707
WikiPage\getDBkey
getDBkey()
Get the page title in DB key form.This may return a string starting with a hash, if the PageReference...
Definition: WikiPage.php:3671
WikiPage\assertProperPage
assertProperPage()
Code that requires this WikiPage to be a "proper page" in the sense defined by PageIdentity should ca...
Definition: WikiPage.php:608
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:1001
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:996
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:1582
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1097
WikiPage\onArticleEdit
static onArticleEdit(Title $title, RevisionRecord $revRecord=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3243
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:2237
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2226
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:360
Title\getInterwiki
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:998
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1411
Title\isValidRedirectTarget
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition: Title.php:4231
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1299
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:317
MediaWiki\Content\ContentHandlerFactory
Definition: ContentHandlerFactory.php:44
WikiPage\exists
exists()
Definition: WikiPage.php:633
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:191
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3187
ObjectCache\getInstance
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:74
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:747
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:777
WikiPage\purgeInterwikiCheckKey
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3291
WikiPage\$mDataLoaded
bool $mDataLoaded
Definition: WikiPage.php:79
ParserOptions\newCanonical
static newCanonical( $context, $userLang=null)
Creates a "canonical" ParserOptions object.
Definition: ParserOptions.php:1113
MediaWiki\User\UserIdentity\getName
getName()
$title
$title
Definition: testCompression.php:38
SiteStatsUpdate\factory
static factory(array $deltas)
Definition: SiteStatsUpdate.php:71
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:2723
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:680
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:911
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:1742
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:1376
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:1018
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:708
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:438
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:3056
MediaWiki\Revision\RevisionRecord\getSlots
getSlots()
Returns the slots defined for this revision.
Definition: RevisionRecord.php:222
Wikimedia\Rdbms\LoadBalancer
Database connection, tracking, load balancing, and transaction manager for a cluster.
Definition: LoadBalancer.php:43
$wgPageLanguageUseDB
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
Definition: DefaultSettings.php:2554
Category\newFromTitle
static newFromTitle( $title)
Factory function.
Definition: Category.php:153
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:706
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:620
$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:3543
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:2662
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:1125
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition: WikiPage.php:886
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
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:128
WikiPage\newFromID
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:221
WikiPage\isNew
isNew()
Tests if the page is new (only has one revision).
Definition: WikiPage.php:690
$wgSitename
$wgSitename
Name of the site.
Definition: DefaultSettings.php:80
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:3629
SCHEMA_COMPAT_WRITE_TEMP
const SCHEMA_COMPAT_WRITE_TEMP
Definition: Defines.php:264
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
LinksDeletionUpdate
Update object handling the cleanup of links tables after a page was deleted.
Definition: LinksDeletionUpdate.php:27
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:1884
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3341
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:237
WikiPage\canExist
canExist()
Definition: WikiPage.php:3687
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:6389
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:373
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:802
WikiPage\setLastEdit
setLastEdit(RevisionRecord $revRecord)
Set the latest revision.
Definition: WikiPage.php:853
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:2538
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1196
WikiPage\getPageUpdaterFactory
getPageUpdaterFactory()
Definition: WikiPage.php:264
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:555
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:6183
RefreshLinksJob\newDynamic
static newDynamic(PageIdentity $page, array $params)
Definition: RefreshLinksJob.php:82
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2598
Title
Represents a title within MediaWiki.
Definition: Title.php:49
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.
LinksUpdate\queueRecursiveJobsForTable
static queueRecursiveJobsForTable(PageIdentity $page, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
Definition: LinksUpdate.php:392
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:72
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:1112
WikiPage\getUser
getUser( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:925
$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:206
WikiPage\doDeleteUpdates
doDeleteUpdates( $id, Content $content=null, RevisionRecord $revRecord=null, UserIdentity $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:3084
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:50
WebRequest\getRequestId
static getRequestId()
Get the current request ID.
Definition: WebRequest.php:332
WikiPage\$mId
int $mId
Definition: WikiPage.php:129
DeletePageJob
Class DeletePageJob.
Definition: DeletePageJob.php:8
WikiPage\getWikiDisplayName
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3616
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:247
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:3386
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3638
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:789
MediaWiki\Storage\PageUpdaterFactory
A factory for PageUpdater instances.
Definition: PageUpdaterFactory.php:52
$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:409
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:3603
$wgArticleCountMethod
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article.
Definition: DefaultSettings.php:5108
Category\newFromName
static newFromName( $name)
Factory function.
Definition: Category.php:133
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:1151
WikiPage\newPageUpdater
newPageUpdater( $performer, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
Definition: WikiPage.php:1804
NS_FILE
const NS_FILE
Definition: Defines.php:70
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:897
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1543
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:658
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:103
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:946
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:456
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:308
WikiPage\__wakeup
__wakeup()
Ensure consistency when unserializing.
Definition: WikiPage.php:3650
MediaWiki\Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:311
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:3041
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:3663
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:2267
WikiPage\getContentHandlerFactory
getContentHandlerFactory()
Definition: WikiPage.php:278
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:325
WikiPage\getRevisionStore
getRevisionStore()
Definition: WikiPage.php:271
$type
$type
Definition: testCompression.php:52
$wgActorTableSchemaMigrationStage
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage, for migration from the temporary table revision_actor_temp to the...
Definition: DefaultSettings.php:2413