MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
29 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
47 use Wikimedia\Assert\Assert;
48 use Wikimedia\Assert\PreconditionException;
49 use Wikimedia\IPUtils;
50 use Wikimedia\NonSerializable\NonSerializableTrait;
54 
62  use NonSerializableTrait;
63  use ProtectedHookAccessorTrait;
65 
66  // Constants for $mDataLoadedFrom and related
67 
73  public $mTitle = null;
74 
80  public $mDataLoaded = false;
81 
87  public $mIsRedirect = false;
88 
94  public $mLatest = false;
95 
101  public $mPreparedEdit = false;
102 
106  protected $mId = null;
107 
112 
116  protected $mRedirectTarget = null;
117 
121  private $mLastRevision = null;
122 
126  protected $mTimestamp = '';
127 
131  protected $mTouched = '19700101000000';
132 
136  protected $mLinksUpdated = '19700101000000';
137 
141  private $derivedDataUpdater = null;
142 
146  public function __construct( PageIdentity $pageIdentity ) {
147  $pageIdentity->assertWiki( PageIdentity::LOCAL );
148 
149  // TODO: remove the need for casting to Title.
150  $title = Title::castFromPageIdentity( $pageIdentity );
151  if ( !$title->canExist() ) {
152  // TODO: In order to allow WikiPage to implement ProperPageIdentity,
153  // throw here to prevent construction of a WikiPage that doesn't
154  // represent a proper page.
156  "WikiPage constructed on a Title that cannot exist as a page: $title",
157  '1.36'
158  );
159  }
160 
161  $this->mTitle = $title;
162  }
163 
168  public function __clone() {
169  $this->mTitle = clone $this->mTitle;
170  }
171 
183  public static function factory( PageIdentity $pageIdentity ) {
184  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $pageIdentity );
185  }
186 
198  public static function newFromID( $id, $from = 'fromdb' ) {
199  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromID( $id, $from );
200  }
201 
214  public static function newFromRow( $row, $from = 'fromdb' ) {
215  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromRow( $row, $from );
216  }
217 
224  public static function convertSelectType( $type ) {
225  switch ( $type ) {
226  case 'fromdb':
227  return self::READ_NORMAL;
228  case 'fromdbmaster':
229  return self::READ_LATEST;
230  case 'forupdate':
231  return self::READ_LOCKING;
232  default:
233  // It may already be an integer or whatever else
234  return $type;
235  }
236  }
237 
241  private function getRevisionStore() {
242  return MediaWikiServices::getInstance()->getRevisionStore();
243  }
244 
248  private function getRevisionRenderer() {
249  return MediaWikiServices::getInstance()->getRevisionRenderer();
250  }
251 
255  private function getSlotRoleRegistry() {
256  return MediaWikiServices::getInstance()->getSlotRoleRegistry();
257  }
258 
263  return MediaWikiServices::getInstance()->getContentHandlerFactory();
264  }
265 
269  private function getParserCache() {
270  return MediaWikiServices::getInstance()->getParserCache();
271  }
272 
276  private function getDBLoadBalancer() {
277  return MediaWikiServices::getInstance()->getDBLoadBalancer();
278  }
279 
286  public function getActionOverrides() {
287  return $this->getContentHandler()->getActionOverrides();
288  }
289 
299  public function getContentHandler() {
300  return $this->getContentHandlerFactory()
301  ->getContentHandler( $this->getContentModel() );
302  }
303 
308  public function getTitle() {
309  return $this->mTitle;
310  }
311 
316  public function clear() {
317  $this->mDataLoaded = false;
318  $this->mDataLoadedFrom = self::READ_NONE;
319 
320  $this->clearCacheFields();
321  }
322 
327  protected function clearCacheFields() {
328  $this->mId = null;
329  $this->mRedirectTarget = null; // Title object if set
330  $this->mLastRevision = null; // Latest revision
331  $this->mTouched = '19700101000000';
332  $this->mLinksUpdated = '19700101000000';
333  $this->mTimestamp = '';
334  $this->mIsRedirect = false;
335  $this->mLatest = false;
336  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
337  // checks the requested rev ID and content against the cached one. For most
338  // content types, the output should not change during the lifetime of this cache.
339  // Clearing it can cause extra parses on edit for no reason.
340  }
341 
347  public function clearPreparedEdit() {
348  $this->mPreparedEdit = false;
349  }
350 
360  public static function getQueryInfo() {
361  global $wgPageLanguageUseDB;
362 
363  $ret = [
364  'tables' => [ 'page' ],
365  'fields' => [
366  'page_id',
367  'page_namespace',
368  'page_title',
369  'page_restrictions',
370  'page_is_redirect',
371  'page_is_new',
372  'page_random',
373  'page_touched',
374  'page_links_updated',
375  'page_latest',
376  'page_len',
377  'page_content_model',
378  ],
379  'joins' => [],
380  ];
381 
382  if ( $wgPageLanguageUseDB ) {
383  $ret['fields'][] = 'page_lang';
384  }
385 
386  return $ret;
387  }
388 
396  protected function pageData( $dbr, $conditions, $options = [] ) {
397  $pageQuery = self::getQueryInfo();
398 
399  $this->getHookRunner()->onArticlePageDataBefore(
400  $this, $pageQuery['fields'], $pageQuery['tables'], $pageQuery['joins'] );
401 
402  $row = $dbr->selectRow(
403  $pageQuery['tables'],
404  $pageQuery['fields'],
405  $conditions,
406  __METHOD__,
407  $options,
408  $pageQuery['joins']
409  );
410 
411  $this->getHookRunner()->onArticlePageDataAfter( $this, $row );
412 
413  return $row;
414  }
415 
425  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
426  if ( !$title->canExist() ) {
427  return false;
428  }
429 
430  return $this->pageData( $dbr, [
431  'page_namespace' => $title->getNamespace(),
432  'page_title' => $title->getDBkey() ], $options );
433  }
434 
443  public function pageDataFromId( $dbr, $id, $options = [] ) {
444  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
445  }
446 
459  public function loadPageData( $from = 'fromdb' ) {
460  if ( !$this->mTitle->canExist() ) {
461  // NOTE: If and when WikiPage implements PageIdentity but not yet ProperPageIdentity,
462  // throw here to prevent usage of a WikiPage that doesn't
463  // represent a proper page.
464  // NOTE: The constructor will already have triggered a warning, but seeing how
465  // bad instances of WikiPage are used will be helpful.
467  "Accessing WikiPage that cannot exist as a page: {$this->mTitle}. ",
468  '1.36'
469  );
470  }
471 
472  $from = self::convertSelectType( $from );
473  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
474  // We already have the data from the correct location, no need to load it twice.
475  return;
476  }
477 
478  if ( is_int( $from ) ) {
479  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
480  $loadBalancer = $this->getDBLoadBalancer();
481  $db = $loadBalancer->getConnection( $index );
482  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
483 
484  if ( !$data
485  && $index == DB_REPLICA
486  && $loadBalancer->getServerCount() > 1
487  && $loadBalancer->hasOrMadeRecentMasterChanges()
488  ) {
489  $from = self::READ_LATEST;
490  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
491  $db = $loadBalancer->getConnection( $index );
492  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
493  }
494  } else {
495  // No idea from where the caller got this data, assume replica DB.
496  $data = $from;
497  $from = self::READ_NORMAL;
498  }
499 
500  $this->loadFromRow( $data, $from );
501  }
502 
516  public function wasLoadedFrom( $from ) {
517  $from = self::convertSelectType( $from );
518 
519  if ( !is_int( $from ) ) {
520  // No idea from where the caller got this data, assume replica DB.
521  $from = self::READ_NORMAL;
522  }
523 
524  if ( $from <= $this->mDataLoadedFrom ) {
525  return true;
526  }
527 
528  return false;
529  }
530 
542  public function loadFromRow( $data, $from ) {
543  $lc = MediaWikiServices::getInstance()->getLinkCache();
544  $lc->clearLink( $this->mTitle );
545 
546  if ( $data ) {
547  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
548 
549  $this->mTitle->loadFromRow( $data );
550 
551  // Old-fashioned restrictions
552  $this->mTitle->loadRestrictions( $data->page_restrictions );
553 
554  $this->mId = intval( $data->page_id );
555  $this->mTouched = MWTimestamp::convert( TS_MW, $data->page_touched );
556  $this->mLinksUpdated = $data->page_links_updated === null
557  ? null
558  : MWTimestamp::convert( TS_MW, $data->page_links_updated );
559  $this->mIsRedirect = intval( $data->page_is_redirect );
560  $this->mLatest = intval( $data->page_latest );
561  // T39225: $latest may no longer match the cached latest RevisionRecord object.
562  // Double-check the ID of any cached latest RevisionRecord object for consistency.
563  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
564  $this->mLastRevision = null;
565  $this->mTimestamp = '';
566  }
567  } else {
568  $lc->addBadLinkObj( $this->mTitle );
569 
570  $this->mTitle->loadFromRow( false );
571 
572  $this->clearCacheFields();
573 
574  $this->mId = 0;
575  }
576 
577  $this->mDataLoaded = true;
578  $this->mDataLoadedFrom = self::convertSelectType( $from );
579  }
580 
592  private function assertProperPage() {
593  Assert::precondition(
594  $this->mTitle->canExist(),
595  'This WikiPage instance does not represent a proper page!'
596  );
597  }
598 
604  public function getId( $wikiId = self::LOCAL ): int {
605  $this->assertWiki( $wikiId );
606  $this->assertProperPage();
607 
608  if ( !$this->mDataLoaded ) {
609  $this->loadPageData();
610  }
611  return $this->mId;
612  }
613 
617  public function exists(): bool {
618  if ( !$this->mDataLoaded ) {
619  $this->loadPageData();
620  }
621  return $this->mId > 0;
622  }
623 
632  public function hasViewableContent() {
633  return $this->mTitle->isKnown();
634  }
635 
641  public function isRedirect() {
642  if ( !$this->mDataLoaded ) {
643  $this->loadPageData();
644  }
645 
646  return (bool)$this->mIsRedirect;
647  }
648 
659  public function getContentModel() {
660  if ( $this->exists() ) {
661  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
662 
663  return $cache->getWithSetCallback(
664  $cache->makeKey( 'page-content-model', $this->getLatest() ),
665  $cache::TTL_MONTH,
666  function () {
667  $rev = $this->getRevisionRecord();
668  if ( $rev ) {
669  // Look at the revision's actual content model
670  $slot = $rev->getSlot(
671  SlotRecord::MAIN,
672  RevisionRecord::RAW
673  );
674  return $slot->getModel();
675  } else {
676  LoggerFactory::getInstance( 'wikipage' )->warning(
677  'Page exists but has no (visible) revisions!',
678  [
679  'page-title' => $this->mTitle->getPrefixedDBkey(),
680  'page-id' => $this->getId(),
681  ]
682  );
683  return $this->mTitle->getContentModel();
684  }
685  },
686  [ 'pcTTL' => $cache::TTL_PROC_LONG ]
687  );
688  }
689 
690  // use the default model for this page
691  return $this->mTitle->getContentModel();
692  }
693 
698  public function checkTouched() {
699  if ( !$this->mDataLoaded ) {
700  $this->loadPageData();
701  }
702  return ( $this->mId && !$this->mIsRedirect );
703  }
704 
709  public function getTouched() {
710  if ( !$this->mDataLoaded ) {
711  $this->loadPageData();
712  }
713  return $this->mTouched;
714  }
715 
720  public function getLinksTimestamp() {
721  if ( !$this->mDataLoaded ) {
722  $this->loadPageData();
723  }
724  return $this->mLinksUpdated;
725  }
726 
731  public function getLatest() {
732  if ( !$this->mDataLoaded ) {
733  $this->loadPageData();
734  }
735  return (int)$this->mLatest;
736  }
737 
744  public function getOldestRevision() {
745  wfDeprecated( __METHOD__, '1.35' );
746  $rev = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
747  return $rev ? new Revision( $rev ) : null;
748  }
749 
754  protected function loadLastEdit() {
755  if ( $this->mLastRevision !== null ) {
756  return; // already loaded
757  }
758 
759  if ( !$this->mTitle->canExist() ) {
760  // NOTE: If and when WikiPage implements PageIdentity but not yet ProperPageIdentity,
761  // throw here to prevent usage of a WikiPage that doesn't
762  // represent a proper page.
763  // NOTE: The constructor will already have triggered a warning, but seeing how
764  // bad instances of WikiPage are used will be helpful.
766  "Accessing WikiPage that cannot exist as a page: {$this->mTitle}. ",
767  '1.36'
768  );
769  }
770 
771  $latest = $this->getLatest();
772  if ( !$latest ) {
773  return; // page doesn't exist or is missing page_latest info
774  }
775 
776  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
777  // T39225: if session S1 loads the page row FOR UPDATE, the result always
778  // includes the latest changes committed. This is true even within REPEATABLE-READ
779  // transactions, where S1 normally only sees changes committed before the first S1
780  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
781  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
782  // happened after the first S1 SELECT.
783  // https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html#isolevel_repeatable-read
784  $revision = $this->getRevisionStore()
785  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LOCKING );
786  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
787  // Bug T93976: if page_latest was loaded from the master, fetch the
788  // revision from there as well, as it may not exist yet on a replica DB.
789  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
790  $revision = $this->getRevisionStore()
791  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LATEST );
792  } else {
793  $revision = $this->getRevisionStore()->getKnownCurrentRevision( $this->getTitle(), $latest );
794  }
795 
796  if ( $revision ) { // sanity
797  $this->setLastEdit( $revision );
798  }
799  }
800 
805  private function setLastEdit( RevisionRecord $revRecord ) {
806  $this->mLastRevision = $revRecord;
807  $this->mTimestamp = $revRecord->getTimestamp();
808  }
809 
815  public function getRevision() {
816  wfDeprecated( __METHOD__, '1.35' );
817  $this->loadLastEdit();
818  if ( $this->mLastRevision ) {
819  return new Revision( $this->mLastRevision );
820  }
821  return null;
822  }
823 
829  public function getRevisionRecord() {
830  $this->loadLastEdit();
831  if ( $this->mLastRevision ) {
832  return $this->mLastRevision;
833  }
834  return null;
835  }
836 
850  public function getContent( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
851  $this->loadLastEdit();
852  if ( $this->mLastRevision ) {
853  return $this->mLastRevision->getContent( SlotRecord::MAIN, $audience, $performer );
854  }
855  return null;
856  }
857 
861  public function getTimestamp() {
862  // Check if the field has been filled by WikiPage::setTimestamp()
863  if ( !$this->mTimestamp ) {
864  $this->loadLastEdit();
865  }
866 
867  return MWTimestamp::convert( TS_MW, $this->mTimestamp );
868  }
869 
875  public function setTimestamp( $ts ) {
876  $this->mTimestamp = MWTimestamp::convert( TS_MW, $ts );
877  }
878 
889  public function getUser( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
890  $this->loadLastEdit();
891  if ( $this->mLastRevision ) {
892  $revUser = $this->mLastRevision->getUser( $audience, $performer );
893  return $revUser ? $revUser->getId() : 0;
894  } else {
895  return -1;
896  }
897  }
898 
910  public function getCreator( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
911  $revRecord = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
912  if ( $revRecord ) {
913  return $revRecord->getUser( $audience, $performer );
914  } else {
915  return null;
916  }
917  }
918 
929  public function getUserText( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
930  $this->loadLastEdit();
931  if ( $this->mLastRevision ) {
932  $revUser = $this->mLastRevision->getUser( $audience, $performer );
933  return $revUser ? $revUser->getName() : '';
934  } else {
935  return '';
936  }
937  }
938 
950  public function getComment( $audience = RevisionRecord::FOR_PUBLIC, Authority $performer = null ) {
951  $this->loadLastEdit();
952  if ( $this->mLastRevision ) {
953  $revComment = $this->mLastRevision->getComment( $audience, $performer );
954  return $revComment ? $revComment->text : '';
955  } else {
956  return '';
957  }
958  }
959 
965  public function getMinorEdit() {
966  $this->loadLastEdit();
967  if ( $this->mLastRevision ) {
968  return $this->mLastRevision->isMinor();
969  } else {
970  return false;
971  }
972  }
973 
982  public function isCountable( $editInfo = false ) {
983  global $wgArticleCountMethod;
984 
985  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
986 
987  if ( !$this->mTitle->isContentPage() ) {
988  return false;
989  }
990 
991  if ( $editInfo ) {
992  // NOTE: only the main slot can make a page a redirect
993  $content = $editInfo->pstContent;
994  } else {
995  $content = $this->getContent();
996  }
997 
998  if ( !$content || $content->isRedirect() ) {
999  return false;
1000  }
1001 
1002  $hasLinks = null;
1003 
1004  if ( $wgArticleCountMethod === 'link' ) {
1005  // nasty special case to avoid re-parsing to detect links
1006 
1007  if ( $editInfo ) {
1008  // ParserOutput::getLinks() is a 2D array of page links, so
1009  // to be really correct we would need to recurse in the array
1010  // but the main array should only have items in it if there are
1011  // links.
1012  $hasLinks = (bool)count( $editInfo->output->getLinks() );
1013  } else {
1014  // NOTE: keep in sync with RevisionRenderer::getLinkCount
1015  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
1016  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', '1',
1017  [ 'pl_from' => $this->getId() ], __METHOD__ );
1018  }
1019  }
1020 
1021  // TODO: MCR: determine $hasLinks for each slot, and use that info
1022  // with that slot's Content's isCountable method. That requires per-
1023  // slot ParserOutput in the ParserCache, or per-slot info in the
1024  // pagelinks table.
1025  return $content->isCountable( $hasLinks );
1026  }
1027 
1035  public function getRedirectTarget() {
1036  if ( !$this->mTitle->isRedirect() ) {
1037  return null;
1038  }
1039 
1040  if ( $this->mRedirectTarget !== null ) {
1041  return $this->mRedirectTarget;
1042  }
1043 
1044  // Query the redirect table
1045  $dbr = wfGetDB( DB_REPLICA );
1046  $row = $dbr->selectRow( 'redirect',
1047  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1048  [ 'rd_from' => $this->getId() ],
1049  __METHOD__
1050  );
1051 
1052  // rd_fragment and rd_interwiki were added later, populate them if empty
1053  if ( $row && $row->rd_fragment !== null && $row->rd_interwiki !== null ) {
1054  // (T203942) We can't redirect to Media namespace because it's virtual.
1055  // We don't want to modify Title objects farther down the
1056  // line. So, let's fix this here by changing to File namespace.
1057  if ( $row->rd_namespace == NS_MEDIA ) {
1058  $namespace = NS_FILE;
1059  } else {
1060  $namespace = $row->rd_namespace;
1061  }
1062  $this->mRedirectTarget = Title::makeTitle(
1063  $namespace, $row->rd_title,
1064  $row->rd_fragment, $row->rd_interwiki
1065  );
1066  return $this->mRedirectTarget;
1067  }
1068 
1069  // This page doesn't have an entry in the redirect table
1070  $this->mRedirectTarget = $this->insertRedirect();
1071  return $this->mRedirectTarget;
1072  }
1073 
1082  public function insertRedirect() {
1083  $content = $this->getContent();
1084  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1085  if ( !$retval ) {
1086  return null;
1087  }
1088 
1089  // Update the DB post-send if the page has not cached since now
1090  $latest = $this->getLatest();
1092  function () use ( $retval, $latest ) {
1093  $this->insertRedirectEntry( $retval, $latest );
1094  },
1095  DeferredUpdates::POSTSEND,
1096  wfGetDB( DB_MASTER )
1097  );
1098 
1099  return $retval;
1100  }
1101 
1108  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1109  $dbw = wfGetDB( DB_MASTER );
1110  $dbw->startAtomic( __METHOD__ );
1111 
1112  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1113  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1114  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1115  $dbw->upsert(
1116  'redirect',
1117  [
1118  'rd_from' => $this->getId(),
1119  'rd_namespace' => $rt->getNamespace(),
1120  'rd_title' => $rt->getDBkey(),
1121  'rd_fragment' => $truncatedFragment,
1122  'rd_interwiki' => $rt->getInterwiki(),
1123  ],
1124  'rd_from',
1125  [
1126  'rd_namespace' => $rt->getNamespace(),
1127  'rd_title' => $rt->getDBkey(),
1128  'rd_fragment' => $truncatedFragment,
1129  'rd_interwiki' => $rt->getInterwiki(),
1130  ],
1131  __METHOD__
1132  );
1133  $success = true;
1134  } else {
1135  $success = false;
1136  }
1137 
1138  $dbw->endAtomic( __METHOD__ );
1139 
1140  return $success;
1141  }
1142 
1148  public function followRedirect() {
1149  return $this->getRedirectURL( $this->getRedirectTarget() );
1150  }
1151 
1159  public function getRedirectURL( $rt ) {
1160  if ( !$rt ) {
1161  return false;
1162  }
1163 
1164  if ( $rt->isExternal() ) {
1165  if ( $rt->isLocal() ) {
1166  // Offsite wikis need an HTTP redirect.
1167  // This can be hard to reverse and may produce loops,
1168  // so they may be disabled in the site configuration.
1169  $source = $this->mTitle->getFullURL( 'redirect=no' );
1170  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1171  } else {
1172  // External pages without "local" bit set are not valid
1173  // redirect targets
1174  return false;
1175  }
1176  }
1177 
1178  if ( $rt->isSpecialPage() ) {
1179  // Gotta handle redirects to special pages differently:
1180  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1181  // Some pages are not valid targets.
1182  if ( $rt->isValidRedirectTarget() ) {
1183  return $rt->getFullURL();
1184  } else {
1185  return false;
1186  }
1187  }
1188 
1189  return $rt;
1190  }
1191 
1197  public function getContributors() {
1198  // @todo: This is expensive; cache this info somewhere.
1199 
1200  $dbr = wfGetDB( DB_REPLICA );
1201 
1202  $actorMigration = ActorMigration::newMigration();
1203  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1204 
1205  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1206 
1207  $revactor_actor = $actorQuery['fields']['rev_actor'];
1208  $fields = [
1209  'user_id' => $actorQuery['fields']['rev_user'],
1210  'user_name' => $actorQuery['fields']['rev_user_text'],
1211  'actor_id' => "MIN($revactor_actor)",
1212  'user_real_name' => 'MIN(user_real_name)',
1213  'timestamp' => 'MAX(rev_timestamp)',
1214  ];
1215 
1216  $conds = [ 'rev_page' => $this->getId() ];
1217 
1218  // The user who made the top revision gets credited as "this page was last edited by
1219  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1220  $user = $this->getUser()
1221  ? User::newFromId( $this->getUser() )
1222  : User::newFromName( $this->getUserText(), false );
1223  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1224 
1225  // Username hidden?
1226  $conds[] = "{$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER )} = 0";
1227 
1228  $jconds = [
1229  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1230  ] + $actorQuery['joins'];
1231 
1232  $options = [
1233  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1234  'ORDER BY' => 'timestamp DESC',
1235  ];
1236 
1237  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1238  return new UserArrayFromResult( $res );
1239  }
1240 
1248  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1249  // NOTE: Keep in sync with ParserOutputAccess::shouldUseCache().
1250  // TODO: Once ParserOutputAccess is stable, deprecated this method.
1251  return $this->exists()
1252  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1253  && $this->getContentHandler()->isParserCacheSupported();
1254  }
1255 
1271  public function getParserOutput(
1272  ParserOptions $parserOptions, $oldid = null, $noCache = false
1273  ) {
1274  if ( $oldid ) {
1275  $revision = $this->getRevisionStore()->getRevisionByTitle( $this->getTitle(), $oldid );
1276 
1277  if ( !$revision ) {
1278  return false;
1279  }
1280  } else {
1281  $revision = $this->getRevisionRecord();
1282  }
1283 
1284  $options = $noCache ? ParserOutputAccess::OPT_NO_CACHE : 0;
1285 
1286  $status = MediaWikiServices::getInstance()->getParserOutputAccess()->getParserOutput(
1287  $this, $parserOptions, $revision, $options
1288  );
1289  return $status->isOK() ? $status->getValue() : false; // convert null to false
1290  }
1291 
1297  public function doViewUpdates( User $user, $oldid = 0 ) {
1298  if ( wfReadOnly() ) {
1299  return;
1300  }
1301 
1302  // Update newtalk / watchlist notification status;
1303  // Avoid outage if the master is not reachable by using a deferred updated
1305  function () use ( $user, $oldid ) {
1306  $this->getHookRunner()->onPageViewUpdates( $this, $user );
1307 
1308  $user->clearNotification( $this->mTitle, $oldid );
1309  },
1310  DeferredUpdates::PRESEND
1311  );
1312  }
1313 
1320  public function doPurge() {
1321  if ( !$this->getHookRunner()->onArticlePurge( $this ) ) {
1322  return false;
1323  }
1324 
1325  $this->mTitle->invalidateCache();
1326 
1327  // Clear file cache and send purge after above page_touched update was committed
1328  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1329  $hcu->purgeTitleUrls( $this->mTitle, $hcu::PURGE_PRESEND );
1330 
1331  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) {
1332  MediaWikiServices::getInstance()->getMessageCache()
1333  ->updateMessageOverride( $this->mTitle, $this->getContent() );
1334  }
1335 
1336  return true;
1337  }
1338 
1355  public function insertOn( $dbw, $pageId = null ) {
1356  $this->assertProperPage();
1357 
1358  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1359  $dbw->insert(
1360  'page',
1361  [
1362  'page_namespace' => $this->mTitle->getNamespace(),
1363  'page_title' => $this->mTitle->getDBkey(),
1364  'page_restrictions' => '',
1365  'page_is_redirect' => 0, // Will set this shortly...
1366  'page_is_new' => 1,
1367  'page_random' => wfRandom(),
1368  'page_touched' => $dbw->timestamp(),
1369  'page_latest' => 0, // Fill this in shortly...
1370  'page_len' => 0, // Fill this in shortly...
1371  ] + $pageIdForInsert,
1372  __METHOD__,
1373  [ 'IGNORE' ]
1374  );
1375 
1376  if ( $dbw->affectedRows() > 0 ) {
1377  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1378  $this->mId = $newid;
1379  $this->mTitle->resetArticleID( $newid );
1380 
1381  return $newid;
1382  } else {
1383  return false; // nothing changed
1384  }
1385  }
1386 
1402  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1403  $lastRevIsRedirect = null
1404  ) {
1405  // TODO: move into PageUpdater or PageStore
1406  // NOTE: when doing that, make sure cached fields get reset in doEditContent,
1407  // and in the compat stub!
1408 
1409  // Assertion to try to catch T92046
1410  if ( (int)$revision->getId() === 0 ) {
1411  throw new InvalidArgumentException(
1412  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1413  );
1414  }
1415 
1416  if ( $revision instanceof Revision ) {
1417  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
1418  $revision = $revision->getRevisionRecord();
1419  }
1420 
1421  $content = $revision->getContent( SlotRecord::MAIN );
1422  $len = $content ? $content->getSize() : 0;
1423  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1424 
1425  $conditions = [ 'page_id' => $this->getId() ];
1426 
1427  if ( $lastRevision !== null ) {
1428  // An extra check against threads stepping on each other
1429  $conditions['page_latest'] = $lastRevision;
1430  }
1431 
1432  $revId = $revision->getId();
1433  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1434 
1435  $model = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel();
1436 
1437  $row = [ /* SET */
1438  'page_latest' => $revId,
1439  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1440  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1441  'page_is_redirect' => $rt !== null ? 1 : 0,
1442  'page_len' => $len,
1443  'page_content_model' => $model,
1444  ];
1445 
1446  $dbw->update( 'page',
1447  $row,
1448  $conditions,
1449  __METHOD__ );
1450 
1451  $result = $dbw->affectedRows() > 0;
1452  if ( $result ) {
1453  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1454  $this->setLastEdit( $revision );
1455  $this->mLatest = $revision->getId();
1456  $this->mIsRedirect = (bool)$rt;
1457  // Update the LinkCache.
1458  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1459  $linkCache->addGoodLinkObj(
1460  $this->getId(),
1461  $this->mTitle,
1462  $len,
1463  $this->mIsRedirect,
1464  $this->mLatest,
1465  $model
1466  );
1467  }
1468 
1469  return $result;
1470  }
1471 
1483  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1484  // Always update redirects (target link might have changed)
1485  // Update/Insert if we don't know if the last revision was a redirect or not
1486  // Delete if changing from redirect to non-redirect
1487  $isRedirect = $redirectTitle !== null;
1488 
1489  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1490  return true;
1491  }
1492 
1493  if ( $isRedirect ) {
1494  $success = $this->insertRedirectEntry( $redirectTitle );
1495  } else {
1496  // This is not a redirect, remove row from redirect table
1497  $where = [ 'rd_from' => $this->getId() ];
1498  $dbw->delete( 'redirect', $where, __METHOD__ );
1499  $success = true;
1500  }
1501 
1502  if ( $this->getTitle()->getNamespace() === NS_FILE ) {
1503  MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
1504  ->invalidateImageRedirect( $this->getTitle() );
1505  }
1506 
1507  return $success;
1508  }
1509 
1520  public function updateIfNewerOn( $dbw, $revision ) {
1521  wfDeprecated( __METHOD__, '1.24' );
1522 
1523  $revisionRecord = $revision->getRevisionRecord();
1524 
1525  $row = $dbw->selectRow(
1526  [ 'revision', 'page' ],
1527  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1528  [
1529  'page_id' => $this->getId(),
1530  'page_latest=rev_id'
1531  ],
1532  __METHOD__
1533  );
1534 
1535  if ( $row ) {
1536  $rowTimestamp = MWTimestamp::convert( TS_MW, $row->rev_timestamp );
1537  if ( $rowTimestamp >= $revisionRecord->getTimestamp() ) {
1538  return false;
1539  }
1540  $prev = $row->rev_id;
1541  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1542  } else {
1543  // No or missing previous revision; mark the page as new
1544  $prev = 0;
1545  $lastRevIsRedirect = null;
1546  }
1547 
1548  $ret = $this->updateRevisionOn(
1549  $dbw,
1550  $revisionRecord,
1551  $prev,
1552  $lastRevIsRedirect
1553  );
1554 
1555  return $ret;
1556  }
1557 
1570  public static function hasDifferencesOutsideMainSlot( $a, $b ) {
1571  if ( $a instanceof Revision ) {
1572  wfDeprecated( __METHOD__ . ' with Revision objects', '1.35' );
1573  $a = $a->getRevisionRecord();
1574  }
1575  if ( $b instanceof Revision ) {
1576  wfDeprecated( __METHOD__ . ' with Revision objects', '1.35' );
1577  $b = $b->getRevisionRecord();
1578  }
1579  $aSlots = $a->getSlots();
1580  $bSlots = $b->getSlots();
1581  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1582 
1583  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1584  }
1585 
1599  public function getUndoContent( Revision $undo, Revision $undoafter ) {
1600  wfDeprecated( __METHOD__, '1.35' );
1601  // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1602 
1603  if ( self::hasDifferencesOutsideMainSlot(
1604  $undo->getRevisionRecord(),
1605  $undoafter->getRevisionRecord()
1606  ) ) {
1607  // Cannot yet undo edits that involve anything other the main slot.
1608  return false;
1609  }
1610 
1611  $handler = $undo->getContentHandler();
1612 
1613  // TODO remove use of Revision objects by deprecating this method entirely
1614  $revRecord = $this->getRevisionRecord();
1615  $revision = $revRecord ? new Revision( $revRecord ) : null;
1616  return $handler->getUndoContent( $revision, $undo, $undoafter );
1617  }
1618 
1629  public function supportsSections() {
1630  return $this->getContentHandler()->supportsSections();
1631  }
1632 
1647  public function replaceSectionContent(
1648  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1649  ) {
1650  $baseRevId = null;
1651  if ( $edittime && $sectionId !== 'new' ) {
1652  $lb = $this->getDBLoadBalancer();
1653  $rev = $this->getRevisionStore()->getRevisionByTimestamp( $this->mTitle, $edittime );
1654  // Try the master if this thread may have just added it.
1655  // This could be abstracted into a Revision method, but we don't want
1656  // to encourage loading of revisions by timestamp.
1657  if ( !$rev
1658  && $lb->getServerCount() > 1
1659  && $lb->hasOrMadeRecentMasterChanges()
1660  ) {
1661  $rev = $this->getRevisionStore()->getRevisionByTimestamp(
1662  $this->mTitle, $edittime, RevisionStore::READ_LATEST );
1663  }
1664  if ( $rev ) {
1665  $baseRevId = $rev->getId();
1666  }
1667  }
1668 
1669  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1670  }
1671 
1685  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1686  $sectionTitle = '', $baseRevId = null
1687  ) {
1688  if ( strval( $sectionId ) === '' ) {
1689  // Whole-page edit; let the whole text through
1690  $newContent = $sectionContent;
1691  } else {
1692  if ( !$this->supportsSections() ) {
1693  throw new MWException( "sections not supported for content model " .
1694  $this->getContentHandler()->getModelID() );
1695  }
1696 
1697  // T32711: always use current version when adding a new section
1698  if ( $baseRevId === null || $sectionId === 'new' ) {
1699  $oldContent = $this->getContent();
1700  } else {
1701  $revRecord = $this->getRevisionStore()->getRevisionById( $baseRevId );
1702  if ( !$revRecord ) {
1703  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1704  $this->getId() . "; section: $sectionId)" );
1705  return null;
1706  }
1707 
1708  $oldContent = $revRecord->getContent( SlotRecord::MAIN );
1709  }
1710 
1711  if ( !$oldContent ) {
1712  wfDebug( __METHOD__ . ": no page text" );
1713  return null;
1714  }
1715 
1716  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1717  }
1718 
1719  return $newContent;
1720  }
1721 
1731  public function checkFlags( $flags ) {
1732  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1733  if ( $this->exists() ) {
1734  $flags |= EDIT_UPDATE;
1735  } else {
1736  $flags |= EDIT_NEW;
1737  }
1738  }
1739 
1740  return $flags;
1741  }
1742 
1746  private function newDerivedDataUpdater() {
1748 
1749  $services = MediaWikiServices::getInstance();
1750  $editResultCache = new EditResultCache(
1751  $services->getMainObjectStash(),
1752  $services->getDBLoadBalancer(),
1753  new ServiceOptions(
1754  EditResultCache::CONSTRUCTOR_OPTIONS,
1755  $services->getMainConfig()
1756  )
1757  );
1758 
1760  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1761  $this->getRevisionStore(),
1762  $this->getRevisionRenderer(),
1763  $this->getSlotRoleRegistry(),
1764  $this->getParserCache(),
1766  $services->getMessageCache(),
1767  $services->getContentLanguage(),
1768  $services->getDBLoadBalancerFactory(),
1769  $this->getContentHandlerFactory(),
1770  $this->getHookContainer(),
1771  $editResultCache
1772  );
1773 
1774  $derivedDataUpdater->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
1777 
1778  return $derivedDataUpdater;
1779  }
1780 
1808  private function getDerivedDataUpdater(
1809  User $forUser = null,
1810  RevisionRecord $forRevision = null,
1811  RevisionSlotsUpdate $forUpdate = null,
1812  $forEdit = false
1813  ) {
1814  if ( !$forRevision && !$forUpdate ) {
1815  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1816  // going to use it with.
1817  $this->derivedDataUpdater = null;
1818  }
1819 
1820  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1821  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1822  // to it did not yet initialize it, because we don't know what data it will be
1823  // initialized with.
1824  $this->derivedDataUpdater = null;
1825  }
1826 
1827  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1828  // However, there is no good way to construct a cache key. We'd need to check against all
1829  // cached instances.
1830 
1831  if ( $this->derivedDataUpdater
1832  && !$this->derivedDataUpdater->isReusableFor(
1833  $forUser,
1834  $forRevision,
1835  $forUpdate,
1836  $forEdit ? $this->getLatest() : null
1837  )
1838  ) {
1839  $this->derivedDataUpdater = null;
1840  }
1841 
1842  if ( !$this->derivedDataUpdater ) {
1843  $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1844  }
1845 
1847  }
1848 
1864  public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1865  $this->assertProperPage();
1866 
1867  $config = MediaWikiServices::getInstance()->getMainConfig();
1868 
1869  $pageUpdater = new PageUpdater(
1870  $user,
1871  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1872  $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1873  $this->getDBLoadBalancer(),
1874  $this->getRevisionStore(),
1875  $this->getSlotRoleRegistry(),
1876  $this->getContentHandlerFactory(),
1877  $this->getHookContainer(),
1878  new ServiceOptions(
1879  PageUpdater::CONSTRUCTOR_OPTIONS,
1880  $config
1881  ),
1883  );
1884 
1885  $pageUpdater->setUsePageCreationLog( $config->get( 'PageCreationLog' ) );
1886  $pageUpdater->setAjaxEditStash( $config->get( 'AjaxEditStash' ) );
1887  $pageUpdater->setUseAutomaticEditSummaries(
1888  $config->get( 'UseAutomaticEditSummaries' )
1889  );
1890 
1891  return $pageUpdater;
1892  }
1893 
1961  public function doEditContent(
1962  Content $content, $summary, $flags = 0, $originalRevId = false,
1963  Authority $performer = null, $serialFormat = null, $tags = [], $undidRevId = 0
1964  ) {
1965  global $wgUser;
1966 
1967  if ( !$performer ) {
1968  $performer = $wgUser;
1969  }
1970 
1971  return $this->doUserEditContent(
1972  $content, $performer, $summary, $flags, $originalRevId, $tags, $undidRevId
1973  );
1974  }
1975 
2036  public function doUserEditContent(
2037  Content $content,
2038  Authority $performer,
2039  $summary,
2040  $flags = 0,
2041  $originalRevId = false,
2042  $tags = [],
2043  $undidRevId = 0
2044  ) {
2046 
2047  if ( !( $summary instanceof CommentStoreComment ) ) {
2048  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
2049  }
2050 
2051  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
2052  // Checking the minoredit right should be done in the same place the 'bot' right is
2053  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
2054  if ( ( $flags & EDIT_MINOR ) && !$performer->isAllowed( 'minoredit' ) ) {
2055  $flags &= ~EDIT_MINOR;
2056  }
2057 
2058  $slotsUpdate = new RevisionSlotsUpdate();
2059  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
2060 
2061  // NOTE: while doUserEditContent() executes, callbacks to getDerivedDataUpdater and
2062  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
2063  // used by this PageUpdater. However, there is no guarantee for this.
2064  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
2065  $updater = $this->newPageUpdater( $user, $slotsUpdate );
2066  $updater->setContent( SlotRecord::MAIN, $content );
2067 
2068  $revisionStore = $this->getRevisionStore();
2069  $originalRevision = $originalRevId ? $revisionStore->getRevisionById( $originalRevId ) : null;
2070  if ( $originalRevision && $undidRevId !== 0 ) {
2071  // Mark it as a revert if it's an undo
2072  $oldestRevertedRev = $revisionStore->getNextRevision( $originalRevision );
2073  if ( $oldestRevertedRev ) {
2074  $updater->markAsRevert(
2075  EditResult::REVERT_UNDO,
2076  $oldestRevertedRev->getId(),
2077  $undidRevId
2078  );
2079  } else {
2080  // We can't find the oldest reverted revision for some reason
2081  $updater->markAsRevert( EditResult::REVERT_UNDO, $undidRevId );
2082  }
2083  } elseif ( $undidRevId !== 0 ) {
2084  // It's an undo, but the original revision is not specified, fall back to just
2085  // marking it as an undo with one revision undone.
2086  $updater->markAsRevert( EditResult::REVERT_UNDO, $undidRevId );
2087  // Try finding the original revision ID by assuming it's the one before the edit
2088  // that is being undone. If the bet fails, $originalRevision is ignored anyway, so
2089  // no damage is done.
2090  $undidRevision = $revisionStore->getRevisionById( $undidRevId );
2091  if ( $undidRevision ) {
2092  $originalRevision = $revisionStore->getPreviousRevision( $undidRevision );
2093  }
2094  }
2095 
2096  // Make sure original revision's content is the same as the new content and save the
2097  // original revision ID.
2098  if ( $originalRevision &&
2099  $originalRevision->getContent( SlotRecord::MAIN )->equals( $content )
2100  ) {
2101  $updater->setOriginalRevisionId( $originalRevision->getId() );
2102  }
2103 
2104  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
2105 
2106  // TODO: this logic should not be in the storage layer, it's here for compatibility
2107  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
2108  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
2109 
2110  if ( $needsPatrol && $performer->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
2111  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
2112  }
2113 
2114  $updater->addTags( $tags );
2115 
2116  $revRec = $updater->saveRevision(
2117  $summary,
2118  $flags
2119  );
2120 
2121  // $revRec will be null if the edit failed, or if no new revision was created because
2122  // the content did not change.
2123  if ( $revRec ) {
2124  // update cached fields
2125  // TODO: this is currently redundant to what is done in updateRevisionOn.
2126  // But updateRevisionOn() should move into PageStore, and then this will be needed.
2127  $this->setLastEdit( $revRec );
2128  $this->mLatest = $revRec->getId();
2129  }
2130 
2131  return $updater->getStatus();
2132  }
2133 
2148  public function makeParserOptions( $context ) {
2149  $options = ParserOptions::newCanonical( $context );
2150 
2151  if ( $this->getTitle()->isConversionTable() ) {
2152  // @todo ConversionTable should become a separate content model, so
2153  // we don't need special cases like this one.
2154  $options->disableContentConversion();
2155  }
2156 
2157  return $options;
2158  }
2159 
2179  public function prepareContentForEdit(
2180  Content $content,
2181  $revision = null,
2182  User $user = null,
2183  $serialFormat = null,
2184  $useCache = true
2185  ) {
2186  global $wgUser;
2187 
2188  if ( !$user ) {
2189  $user = $wgUser;
2190  }
2191 
2192  if ( $revision !== null ) {
2193  if ( $revision instanceof Revision ) {
2194  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
2195  $revision = $revision->getRevisionRecord();
2196  } elseif ( !( $revision instanceof RevisionRecord ) ) {
2197  throw new InvalidArgumentException(
2198  __METHOD__ . ': invalid $revision argument type ' . gettype( $revision ) );
2199  }
2200  }
2201 
2202  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2203  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2204 
2205  if ( !$updater->isUpdatePrepared() ) {
2206  $updater->prepareContent( $user, $slots, $useCache );
2207 
2208  if ( $revision ) {
2209  $updater->prepareUpdate(
2210  $revision,
2211  [
2212  'causeAction' => 'prepare-edit',
2213  'causeAgent' => $user->getName(),
2214  ]
2215  );
2216  }
2217  }
2218 
2219  return $updater->getPreparedEdit();
2220  }
2221 
2251  public function doEditUpdates( $revisionRecord, User $user, array $options = [] ) {
2252  if ( $revisionRecord instanceof Revision ) {
2253  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
2254  $revisionRecord = $revisionRecord->getRevisionRecord();
2255  }
2256  if ( isset( $options['oldrevision'] ) && $options['oldrevision'] instanceof Revision ) {
2257  wfDeprecated(
2258  __METHOD__ . ' with the `oldrevision` option being a ' .
2259  'Revision object',
2260  '1.35'
2261  );
2262  $options['oldrevision'] = $options['oldrevision']->getRevisionRecord();
2263  }
2264 
2265  $options += [
2266  'causeAction' => 'edit-page',
2267  'causeAgent' => $user->getName(),
2268  ];
2269 
2270  $updater = $this->getDerivedDataUpdater( $user, $revisionRecord );
2271 
2272  $updater->prepareUpdate( $revisionRecord, $options );
2273 
2274  $updater->doUpdates();
2275  }
2276 
2290  public function updateParserCache( array $options = [] ) {
2291  $revision = $this->getRevisionRecord();
2292  if ( !$revision || !$revision->getId() ) {
2293  LoggerFactory::getInstance( 'wikipage' )->info(
2294  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2295  );
2296  return;
2297  }
2298  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2299 
2300  $updater = $this->getDerivedDataUpdater( $user, $revision );
2301  $updater->prepareUpdate( $revision, $options );
2302  $updater->doParserCacheUpdate();
2303  }
2304 
2334  public function doSecondaryDataUpdates( array $options = [] ) {
2335  $options['recursive'] = $options['recursive'] ?? true;
2336  $revision = $this->getRevisionRecord();
2337  if ( !$revision || !$revision->getId() ) {
2338  LoggerFactory::getInstance( 'wikipage' )->info(
2339  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2340  );
2341  return;
2342  }
2343  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2344 
2345  $updater = $this->getDerivedDataUpdater( $user, $revision );
2346  $updater->prepareUpdate( $revision, $options );
2347  $updater->doSecondaryDataUpdates( $options );
2348  }
2349 
2364  public function doUpdateRestrictions( array $limit, array $expiry,
2365  &$cascade, $reason, User $user, $tags = null
2366  ) {
2368 
2369  $this->assertProperPage();
2370 
2371  if ( wfReadOnly() ) {
2372  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2373  }
2374 
2375  $this->loadPageData( 'fromdbmaster' );
2376  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2377  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2378  $id = $this->getId();
2379 
2380  if ( !$cascade ) {
2381  $cascade = false;
2382  }
2383 
2384  // Take this opportunity to purge out expired restrictions
2386 
2387  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2388  // we expect a single selection, but the schema allows otherwise.
2389  $isProtected = false;
2390  $protect = false;
2391  $changed = false;
2392 
2393  $dbw = wfGetDB( DB_MASTER );
2394 
2395  foreach ( $restrictionTypes as $action ) {
2396  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2397  $expiry[$action] = 'infinity';
2398  }
2399  if ( !isset( $limit[$action] ) ) {
2400  $limit[$action] = '';
2401  } elseif ( $limit[$action] != '' ) {
2402  $protect = true;
2403  }
2404 
2405  // Get current restrictions on $action
2406  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2407  if ( $current != '' ) {
2408  $isProtected = true;
2409  }
2410 
2411  if ( $limit[$action] != $current ) {
2412  $changed = true;
2413  } elseif ( $limit[$action] != '' ) {
2414  // Only check expiry change if the action is actually being
2415  // protected, since expiry does nothing on an not-protected
2416  // action.
2417  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2418  $changed = true;
2419  }
2420  }
2421  }
2422 
2423  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2424  $changed = true;
2425  }
2426 
2427  // If nothing has changed, do nothing
2428  if ( !$changed ) {
2429  return Status::newGood();
2430  }
2431 
2432  if ( !$protect ) { // No protection at all means unprotection
2433  $revCommentMsg = 'unprotectedarticle-comment';
2434  $logAction = 'unprotect';
2435  } elseif ( $isProtected ) {
2436  $revCommentMsg = 'modifiedarticleprotection-comment';
2437  $logAction = 'modify';
2438  } else {
2439  $revCommentMsg = 'protectedarticle-comment';
2440  $logAction = 'protect';
2441  }
2442 
2443  $logRelationsValues = [];
2444  $logRelationsField = null;
2445  $logParamsDetails = [];
2446 
2447  // Null revision (used for change tag insertion)
2448  $nullRevision = null;
2449 
2450  if ( $id ) { // Protection of existing page
2451  if ( !$this->getHookRunner()->onArticleProtect( $this, $user, $limit, $reason ) ) {
2452  return Status::newGood();
2453  }
2454 
2455  // Only certain restrictions can cascade...
2456  $editrestriction = isset( $limit['edit'] )
2457  ? [ $limit['edit'] ]
2458  : $this->mTitle->getRestrictions( 'edit' );
2459  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2460  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2461  }
2462  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2463  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2464  }
2465 
2466  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2467  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2468  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2469  }
2470  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2471  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2472  }
2473 
2474  // The schema allows multiple restrictions
2475  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2476  $cascade = false;
2477  }
2478 
2479  // insert null revision to identify the page protection change as edit summary
2480  $latest = $this->getLatest();
2481  $nullRevisionRecord = $this->insertNullProtectionRevision(
2482  $revCommentMsg,
2483  $limit,
2484  $expiry,
2485  $cascade,
2486  $reason,
2487  $user
2488  );
2489 
2490  if ( $nullRevisionRecord === null ) {
2491  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2492  }
2493 
2494  $logRelationsField = 'pr_id';
2495 
2496  // T214035: Avoid deadlock on MySQL.
2497  // Do a DELETE by primary key (pr_id) for any existing protection rows.
2498  // On MySQL and derivatives, unconditionally deleting by page ID (pr_page) would.
2499  // place a gap lock if there are no matching rows. This can deadlock when another
2500  // thread modifies protection settings for page IDs in the same gap.
2501  $existingProtectionIds = $dbw->selectFieldValues(
2502  'page_restrictions',
2503  'pr_id',
2504  [
2505  'pr_page' => $id,
2506  'pr_type' => array_map( 'strval', array_keys( $limit ) )
2507  ],
2508  __METHOD__
2509  );
2510 
2511  if ( $existingProtectionIds ) {
2512  $dbw->delete(
2513  'page_restrictions',
2514  [ 'pr_id' => $existingProtectionIds ],
2515  __METHOD__
2516  );
2517  }
2518 
2519  // Update restrictions table
2520  foreach ( $limit as $action => $restrictions ) {
2521  if ( $restrictions != '' ) {
2522  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2523  $dbw->insert(
2524  'page_restrictions',
2525  [
2526  'pr_page' => $id,
2527  'pr_type' => $action,
2528  'pr_level' => $restrictions,
2529  'pr_cascade' => $cascadeValue,
2530  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2531  ],
2532  __METHOD__
2533  );
2534  $logRelationsValues[] = $dbw->insertId();
2535  $logParamsDetails[] = [
2536  'type' => $action,
2537  'level' => $restrictions,
2538  'expiry' => $expiry[$action],
2539  'cascade' => (bool)$cascadeValue,
2540  ];
2541  }
2542  }
2543 
2544  // Clear out legacy restriction fields
2545  $dbw->update(
2546  'page',
2547  [ 'page_restrictions' => '' ],
2548  [ 'page_id' => $id ],
2549  __METHOD__
2550  );
2551 
2552  $this->getHookRunner()->onRevisionFromEditComplete(
2553  $this, $nullRevisionRecord, $latest, $user, $tags );
2554 
2555  // Hook is hard deprecated since 1.35
2556  if ( $this->getHookContainer()->isRegistered( 'NewRevisionFromEditComplete' ) ) {
2557  // Only create the Revision object if neeed
2558  $nullRevision = new Revision( $nullRevisionRecord );
2559  $this->getHookRunner()->onNewRevisionFromEditComplete(
2560  $this, $nullRevision, $latest, $user, $tags );
2561  }
2562 
2563  $this->getHookRunner()->onArticleProtectComplete( $this, $user, $limit, $reason );
2564  } else { // Protection of non-existing page (also known as "title protection")
2565  // Cascade protection is meaningless in this case
2566  $cascade = false;
2567 
2568  if ( $limit['create'] != '' ) {
2569  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2570  $dbw->replace( 'protected_titles',
2571  [ [ 'pt_namespace', 'pt_title' ] ],
2572  [
2573  'pt_namespace' => $this->mTitle->getNamespace(),
2574  'pt_title' => $this->mTitle->getDBkey(),
2575  'pt_create_perm' => $limit['create'],
2576  'pt_timestamp' => $dbw->timestamp(),
2577  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2578  'pt_user' => $user->getId(),
2579  ] + $commentFields, __METHOD__
2580  );
2581  $logParamsDetails[] = [
2582  'type' => 'create',
2583  'level' => $limit['create'],
2584  'expiry' => $expiry['create'],
2585  ];
2586  } else {
2587  $dbw->delete( 'protected_titles',
2588  [
2589  'pt_namespace' => $this->mTitle->getNamespace(),
2590  'pt_title' => $this->mTitle->getDBkey()
2591  ], __METHOD__
2592  );
2593  }
2594  }
2595 
2596  $this->mTitle->flushRestrictions();
2597  InfoAction::invalidateCache( $this->mTitle );
2598 
2599  if ( $logAction == 'unprotect' ) {
2600  $params = [];
2601  } else {
2602  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2603  $params = [
2604  '4::description' => $protectDescriptionLog, // parameter for IRC
2605  '5:bool:cascade' => $cascade,
2606  'details' => $logParamsDetails, // parameter for localize and api
2607  ];
2608  }
2609 
2610  // Update the protection log
2611  $logEntry = new ManualLogEntry( 'protect', $logAction );
2612  $logEntry->setTarget( $this->mTitle );
2613  $logEntry->setComment( $reason );
2614  $logEntry->setPerformer( $user );
2615  $logEntry->setParameters( $params );
2616  if ( $nullRevision !== null ) {
2617  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2618  }
2619  $logEntry->addTags( $tags );
2620  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2621  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2622  }
2623  $logId = $logEntry->insert();
2624  $logEntry->publish( $logId );
2625 
2626  return Status::newGood( $logId );
2627  }
2628 
2643  string $revCommentMsg,
2644  array $limit,
2645  array $expiry,
2646  bool $cascade,
2647  string $reason,
2648  UserIdentity $user
2649  ) : ?RevisionRecord {
2650  $dbw = wfGetDB( DB_MASTER );
2651 
2652  // Prepare a null revision to be added to the history
2653  $editComment = wfMessage(
2654  $revCommentMsg,
2655  $this->mTitle->getPrefixedText(),
2656  $user->getName()
2657  )->inContentLanguage()->text();
2658  if ( $reason ) {
2659  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2660  }
2661  $protectDescription = $this->protectDescription( $limit, $expiry );
2662  if ( $protectDescription ) {
2663  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2664  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2665  ->inContentLanguage()->text();
2666  }
2667  if ( $cascade ) {
2668  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2669  $editComment .= wfMessage( 'brackets' )->params(
2670  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2671  )->inContentLanguage()->text();
2672  }
2673 
2674  $revStore = $this->getRevisionStore();
2675  $comment = CommentStoreComment::newUnsavedComment( $editComment );
2676  $nullRevRecord = $revStore->newNullRevision(
2677  $dbw,
2678  $this->getTitle(),
2679  $comment,
2680  true,
2681  $user
2682  );
2683 
2684  if ( $nullRevRecord ) {
2685  $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
2686 
2687  // Update page record and touch page
2688  $oldLatest = $inserted->getParentId();
2689 
2690  $this->updateRevisionOn( $dbw, $inserted, $oldLatest );
2691 
2692  return $inserted;
2693  } else {
2694  return null;
2695  }
2696  }
2697 
2702  protected function formatExpiry( $expiry ) {
2703  if ( $expiry != 'infinity' ) {
2704  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2705  return wfMessage(
2706  'protect-expiring',
2707  $contLang->timeanddate( $expiry, false, false ),
2708  $contLang->date( $expiry, false, false ),
2709  $contLang->time( $expiry, false, false )
2710  )->inContentLanguage()->text();
2711  } else {
2712  return wfMessage( 'protect-expiry-indefinite' )
2713  ->inContentLanguage()->text();
2714  }
2715  }
2716 
2724  public function protectDescription( array $limit, array $expiry ) {
2725  $protectDescription = '';
2726 
2727  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2728  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2729  # All possible message keys are listed here for easier grepping:
2730  # * restriction-create
2731  # * restriction-edit
2732  # * restriction-move
2733  # * restriction-upload
2734  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2735  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2736  # with '' filtered out. All possible message keys are listed below:
2737  # * protect-level-autoconfirmed
2738  # * protect-level-sysop
2739  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2740  ->inContentLanguage()->text();
2741 
2742  $expiryText = $this->formatExpiry( $expiry[$action] );
2743 
2744  if ( $protectDescription !== '' ) {
2745  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2746  }
2747  $protectDescription .= wfMessage( 'protect-summary-desc' )
2748  ->params( $actionText, $restrictionsText, $expiryText )
2749  ->inContentLanguage()->text();
2750  }
2751 
2752  return $protectDescription;
2753  }
2754 
2766  public function protectDescriptionLog( array $limit, array $expiry ) {
2767  $protectDescriptionLog = '';
2768 
2769  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2770  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2771  $expiryText = $this->formatExpiry( $expiry[$action] );
2772  $protectDescriptionLog .=
2773  $dirMark .
2774  "[$action=$restrictions] ($expiryText)";
2775  }
2776 
2777  return trim( $protectDescriptionLog );
2778  }
2779 
2792  public function isBatchedDelete( $safetyMargin = 0 ) {
2794 
2795  $dbr = wfGetDB( DB_REPLICA );
2796  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2797  $revCount += $safetyMargin;
2798 
2799  return $revCount >= $wgDeleteRevisionsBatchSize;
2800  }
2801 
2827  public function doDeleteArticleReal(
2828  $reason, User $deleter, $suppress = false, $u1 = null, &$error = '', $u2 = null,
2829  $tags = [], $logsubtype = 'delete', $immediate = false
2830  ) {
2831  wfDebug( __METHOD__ );
2832  $this->assertProperPage();
2833 
2834  $status = Status::newGood();
2835 
2836  if ( !$this->getHookRunner()->onArticleDelete(
2837  $this, $deleter, $reason, $error, $status, $suppress )
2838  ) {
2839  if ( $status->isOK() ) {
2840  // Hook aborted but didn't set a fatal status
2841  $status->fatal( 'delete-hook-aborted' );
2842  }
2843  return $status;
2844  }
2845 
2846  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2847  $logsubtype, $immediate );
2848  }
2849 
2866  public function doDeleteArticleBatched(
2867  $reason, $suppress, User $deleter, $tags,
2868  $logsubtype, $immediate = false, $webRequestId = null
2869  ) {
2870  wfDebug( __METHOD__ );
2871 
2872  $status = Status::newGood();
2873 
2874  $dbw = wfGetDB( DB_MASTER );
2875  $dbw->startAtomic( __METHOD__ );
2876 
2877  $this->loadPageData( self::READ_LATEST );
2878  $id = $this->getId();
2879  // T98706: lock the page from various other updates but avoid using
2880  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2881  // the revisions queries (which also JOIN on user). Only lock the page
2882  // row and CAS check on page_latest to see if the trx snapshot matches.
2883  $lockedLatest = $this->lockAndGetLatest();
2884  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2885  $dbw->endAtomic( __METHOD__ );
2886  // Page not there or trx snapshot is stale
2887  $status->error( 'cannotdelete',
2888  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2889  return $status;
2890  }
2891 
2892  // At this point we are now committed to returning an OK
2893  // status unless some DB query error or other exception comes up.
2894  // This way callers don't have to call rollback() if $status is bad
2895  // unless they actually try to catch exceptions (which is rare).
2896 
2897  // we need to remember the old content so we can use it to generate all deletion updates.
2898  $revisionRecord = $this->getRevisionRecord();
2899  try {
2900  $content = $this->getContent( RevisionRecord::RAW );
2901  } catch ( Exception $ex ) {
2902  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2903  . $ex->getMessage() );
2904 
2905  $content = null;
2906  }
2907 
2908  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2909  // one batch of revisions and defer archival of any others to the job queue.
2910  $explictTrxLogged = false;
2911  while ( true ) {
2912  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2913  if ( $done || !$immediate ) {
2914  break;
2915  }
2916  $dbw->endAtomic( __METHOD__ );
2917  if ( $dbw->explicitTrxActive() ) {
2918  // Explict transactions may never happen here in practice. Log to be sure.
2919  if ( !$explictTrxLogged ) {
2920  $explictTrxLogged = true;
2921  LoggerFactory::getInstance( 'wfDebug' )->debug(
2922  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2923  'title' => $this->getTitle()->getText(),
2924  ] );
2925  }
2926  continue;
2927  }
2928  if ( $dbw->trxLevel() ) {
2929  $dbw->commit( __METHOD__ );
2930  }
2931  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2932  $lbFactory->waitForReplication();
2933  $dbw->startAtomic( __METHOD__ );
2934  }
2935 
2936  // If done archiving, also delete the article.
2937  if ( !$done ) {
2938  $dbw->endAtomic( __METHOD__ );
2939 
2940  $jobParams = [
2941  'namespace' => $this->getTitle()->getNamespace(),
2942  'title' => $this->getTitle()->getDBkey(),
2943  'wikiPageId' => $id,
2944  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2945  'reason' => $reason,
2946  'suppress' => $suppress,
2947  'userId' => $deleter->getId(),
2948  'tags' => json_encode( $tags ),
2949  'logsubtype' => $logsubtype,
2950  ];
2951 
2952  $job = new DeletePageJob( $jobParams );
2953  JobQueueGroup::singleton()->push( $job );
2954 
2955  $status->warning( 'delete-scheduled',
2956  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2957  } else {
2958  // Get archivedRevisionCount by db query, because there's no better alternative.
2959  // Jobs cannot pass a count of archived revisions to the next job, because additional
2960  // deletion operations can be started while the first is running. Jobs from each
2961  // gracefully interleave, but would not know about each other's count. Deduplication
2962  // in the job queue to avoid simultaneous deletion operations would add overhead.
2963  // Number of archived revisions cannot be known beforehand, because edits can be made
2964  // while deletion operations are being processed, changing the number of archivals.
2965  $archivedRevisionCount = (int)$dbw->selectField(
2966  'archive', 'COUNT(*)',
2967  [
2968  'ar_namespace' => $this->getTitle()->getNamespace(),
2969  'ar_title' => $this->getTitle()->getDBkey(),
2970  'ar_page_id' => $id
2971  ], __METHOD__
2972  );
2973 
2974  // Clone the title and wikiPage, so we have the information we need when
2975  // we log and run the ArticleDeleteComplete hook.
2976  $logTitle = clone $this->mTitle;
2977  $wikiPageBeforeDelete = clone $this;
2978 
2979  // Now that it's safely backed up, delete it
2980  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2981 
2982  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2983  $logtype = $suppress ? 'suppress' : 'delete';
2984 
2985  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2986  $logEntry->setPerformer( $deleter );
2987  $logEntry->setTarget( $logTitle );
2988  $logEntry->setComment( $reason );
2989  $logEntry->addTags( $tags );
2990  $logid = $logEntry->insert();
2991 
2992  $dbw->onTransactionPreCommitOrIdle(
2993  static function () use ( $logEntry, $logid ) {
2994  // T58776: avoid deadlocks (especially from FileDeleteForm)
2995  $logEntry->publish( $logid );
2996  },
2997  __METHOD__
2998  );
2999 
3000  $dbw->endAtomic( __METHOD__ );
3001 
3002  $this->doDeleteUpdates(
3003  $id,
3004  $content,
3005  $revisionRecord,
3006  $deleter
3007  );
3008 
3009  $this->getHookRunner()->onArticleDeleteComplete(
3010  $wikiPageBeforeDelete,
3011  $deleter,
3012  $reason,
3013  $id,
3014  $content,
3015  $logEntry,
3016  $archivedRevisionCount
3017  );
3018  $status->value = $logid;
3019 
3020  // Show log excerpt on 404 pages rather than just a link
3021  $dbCache = ObjectCache::getInstance( 'db-replicated' );
3022  $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
3023  $dbCache->set( $key, 1, $dbCache::TTL_DAY );
3024  }
3025 
3026  return $status;
3027  }
3028 
3038  protected function archiveRevisions( $dbw, $id, $suppress ) {
3040 
3041  // Given the lock above, we can be confident in the title and page ID values
3042  $namespace = $this->getTitle()->getNamespace();
3043  $dbKey = $this->getTitle()->getDBkey();
3044 
3045  $commentStore = CommentStore::getStore();
3046  $actorMigration = ActorMigration::newMigration();
3047 
3048  $revQuery = $this->getRevisionStore()->getQueryInfo();
3049  $bitfield = false;
3050 
3051  // Bitfields to further suppress the content
3052  if ( $suppress ) {
3053  $bitfield = RevisionRecord::SUPPRESSED_ALL;
3054  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
3055  }
3056 
3057  // For now, shunt the revision data into the archive table.
3058  // Text is *not* removed from the text table; bulk storage
3059  // is left intact to avoid breaking block-compression or
3060  // immutable storage schemes.
3061  // In the future, we may keep revisions and mark them with
3062  // the rev_deleted field, which is reserved for this purpose.
3063 
3064  // Lock rows in `revision` and its temp tables, but not any others.
3065  // Note array_intersect() preserves keys from the first arg, and we're
3066  // assuming $revQuery has `revision` primary and isn't using subtables
3067  // for anything we care about.
3068  $dbw->lockForUpdate(
3069  array_intersect(
3070  $revQuery['tables'],
3071  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
3072  ),
3073  [ 'rev_page' => $id ],
3074  __METHOD__,
3075  [],
3076  $revQuery['joins']
3077  );
3078 
3079  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
3080  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
3081  $res = $dbw->select(
3082  $revQuery['tables'],
3083  $revQuery['fields'],
3084  [ 'rev_page' => $id ],
3085  __METHOD__,
3086  [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
3087  $revQuery['joins']
3088  );
3089 
3090  // Build their equivalent archive rows
3091  $rowsInsert = [];
3092  $revids = [];
3093 
3095  $ipRevIds = [];
3096 
3097  $done = true;
3098  foreach ( $res as $row ) {
3099  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
3100  $done = false;
3101  break;
3102  }
3103 
3104  $comment = $commentStore->getComment( 'rev_comment', $row );
3105  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
3106  $rowInsert = [
3107  'ar_namespace' => $namespace,
3108  'ar_title' => $dbKey,
3109  'ar_timestamp' => $row->rev_timestamp,
3110  'ar_minor_edit' => $row->rev_minor_edit,
3111  'ar_rev_id' => $row->rev_id,
3112  'ar_parent_id' => $row->rev_parent_id,
3113  'ar_len' => $row->rev_len,
3114  'ar_page_id' => $id,
3115  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
3116  'ar_sha1' => $row->rev_sha1,
3117  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
3118  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
3119 
3120  $rowsInsert[] = $rowInsert;
3121  $revids[] = $row->rev_id;
3122 
3123  // Keep track of IP edits, so that the corresponding rows can
3124  // be deleted in the ip_changes table.
3125  if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
3126  $ipRevIds[] = $row->rev_id;
3127  }
3128  }
3129 
3130  // This conditional is just a sanity check
3131  if ( count( $revids ) > 0 ) {
3132  // Copy them into the archive table
3133  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
3134 
3135  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
3136  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
3137  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
3138 
3139  // Also delete records from ip_changes as applicable.
3140  if ( count( $ipRevIds ) > 0 ) {
3141  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
3142  }
3143  }
3144 
3145  return $done;
3146  }
3147 
3154  public function lockAndGetLatest() {
3155  return (int)wfGetDB( DB_MASTER )->selectField(
3156  'page',
3157  'page_latest',
3158  [
3159  'page_id' => $this->getId(),
3160  // Typically page_id is enough, but some code might try to do
3161  // updates assuming the title is the same, so verify that
3162  'page_namespace' => $this->getTitle()->getNamespace(),
3163  'page_title' => $this->getTitle()->getDBkey()
3164  ],
3165  __METHOD__,
3166  [ 'FOR UPDATE' ]
3167  );
3168  }
3169 
3183  public function doDeleteUpdates(
3184  $id, Content $content = null, $revRecord = null, User $user = null
3185  ) {
3186  if ( $revRecord && $revRecord instanceof Revision ) {
3187  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
3188  $revRecord = $revRecord->getRevisionRecord();
3189  }
3190 
3191  if ( $id !== $this->getId() ) {
3192  throw new InvalidArgumentException( 'Mismatching page ID' );
3193  }
3194 
3195  try {
3196  $countable = $this->isCountable();
3197  } catch ( Exception $ex ) {
3198  // fallback for deleting broken pages for which we cannot load the content for
3199  // some reason. Note that doDeleteArticleReal() already logged this problem.
3200  $countable = false;
3201  }
3202 
3203  // Update site status
3205  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3206  ) );
3207 
3208  // Delete pagelinks, update secondary indexes, etc
3209  $updates = $this->getDeletionUpdates( $revRecord ?: $content );
3210  foreach ( $updates as $update ) {
3211  DeferredUpdates::addUpdate( $update );
3212  }
3213 
3214  $causeAgent = $user ? $user->getName() : 'unknown';
3215  // Reparse any pages transcluding this page
3217  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3218  // Reparse any pages including this image
3219  if ( $this->mTitle->getNamespace() === NS_FILE ) {
3221  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3222  }
3223 
3224  // Clear caches
3225  self::onArticleDelete( $this->mTitle );
3226 
3228  $this->mTitle,
3229  $revRecord,
3230  null,
3232  );
3233 
3234  // Reset this object and the Title object
3235  $this->loadFromRow( false, self::READ_LATEST );
3236 
3237  // Search engine
3238  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3239  }
3240 
3272  public function doRollback(
3273  $fromP, $summary, $token, $bot, &$resultDetails, Authority $performer, $tags = null
3274  ) {
3275  $this->assertProperPage();
3276 
3277  $resultDetails = null;
3278 
3279  // Check permissions
3280  $permissionStatus = PermissionStatus::newEmpty();
3281  $performer->authorizeWrite( 'edit', $this->getTitle(), $permissionStatus );
3282  $performer->authorizeWrite( 'rollback', $this->getTitle(), $permissionStatus );
3283 
3284  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
3285  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3286  $permissionStatus->fatal( 'sessionfailure' );
3287  }
3288 
3289  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3290  $permissionStatus->fatal( 'actionthrottledtext' );
3291  }
3292 
3293  // If there were errors, bail out now
3294  if ( !$permissionStatus->isGood() ) {
3295  return $permissionStatus->toLegacyErrorArray();
3296  }
3297 
3298  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $performer, $tags );
3299  }
3300 
3323  public function commitRollback( $fromP, $summary, $bot,
3324  &$resultDetails, Authority $performer, $tags = null
3325  ) {
3327 
3328  $dbw = wfGetDB( DB_MASTER );
3329 
3330  if ( wfReadOnly() ) {
3331  return [ [ 'readonlytext' ] ];
3332  }
3333 
3334  // Begin revision creation cycle by creating a PageUpdater.
3335  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3336  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
3337  $updater = $this->newPageUpdater( $user );
3338  $current = $updater->grabParentRevision();
3339 
3340  if ( $current === null ) {
3341  // Something wrong... no page?
3342  return [ [ 'notanarticle' ] ];
3343  }
3344 
3345  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3346  $legacyCurrentCallback = static function () use ( $current ) {
3347  // Only created when needed
3348  return new Revision( $current );
3349  };
3350  $from = str_replace( '_', ' ', $fromP );
3351 
3352  // User name given should match up with the top revision.
3353  // If the revision's user is not visible, then $from should be empty.
3354  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3355  $resultDetails = new DeprecatablePropertyArray(
3356  [
3357  'current' => $legacyCurrentCallback,
3358  'current-revision-record' => $current,
3359  ],
3360  [ 'current' => '1.35' ],
3361  __METHOD__
3362  );
3363  return [ [ 'alreadyrolled',
3364  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3365  htmlspecialchars( $fromP ),
3366  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3367  ] ];
3368  }
3369 
3370  // Get the last edit not by this person...
3371  // Note: these may not be public values
3372  $actorWhere = ActorMigration::newMigration()->getWhere(
3373  $dbw,
3374  'rev_user',
3375  $current->getUser( RevisionRecord::RAW )
3376  );
3377 
3378  $s = $dbw->selectRow(
3379  [ 'revision' ] + $actorWhere['tables'],
3380  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3381  [
3382  'rev_page' => $current->getPageId(),
3383  'NOT(' . $actorWhere['conds'] . ')',
3384  ],
3385  __METHOD__,
3386  [
3387  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3388  'ORDER BY' => [ 'rev_timestamp DESC', 'rev_id DESC' ]
3389  ],
3390  $actorWhere['joins']
3391  );
3392  if ( $s === false ) {
3393  // No one else ever edited this page
3394  return [ [ 'cantrollback' ] ];
3395  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3396  || $s->rev_deleted & RevisionRecord::DELETED_USER
3397  ) {
3398  // Only admins can see this text
3399  return [ [ 'notvisiblerev' ] ];
3400  }
3401 
3402  // Generate the edit summary if necessary
3403  $target = $this->getRevisionStore()->getRevisionById(
3404  $s->rev_id,
3405  RevisionStore::READ_LATEST
3406  );
3407  if ( empty( $summary ) ) {
3408  if ( !$currentEditorForPublic ) { // no public user name
3409  $summary = wfMessage( 'revertpage-nouser' );
3410  } elseif ( $wgDisableAnonTalk && $current->getUser() === 0 ) {
3411  $summary = wfMessage( 'revertpage-anon' );
3412  } else {
3413  $summary = wfMessage( 'revertpage' );
3414  }
3415  }
3416  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3417 
3418  // Allow the custom summary to use the same args as the default message
3419  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3420  $args = [
3421  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3422  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3423  $s->rev_id,
3424  $contLang->timeanddate( MWTimestamp::convert( TS_MW, $s->rev_timestamp ) ),
3425  $current->getId(),
3426  $contLang->timeanddate( $current->getTimestamp() )
3427  ];
3428  if ( $summary instanceof Message ) {
3429  $summary = $summary->params( $args )->inContentLanguage()->text();
3430  } else {
3431  $summary = wfMsgReplaceArgs( $summary, $args );
3432  }
3433 
3434  // Trim spaces on user supplied text
3435  $summary = trim( $summary );
3436 
3437  // Save
3438  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3439 
3440  if ( $performer->isAllowed( 'minoredit' ) ) {
3441  $flags |= EDIT_MINOR;
3442  }
3443 
3444  if ( $bot && ( $performer->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3445  $flags |= EDIT_FORCE_BOT;
3446  }
3447 
3448  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3449  $currentContent = $current->getContent( SlotRecord::MAIN );
3450  $targetContent = $target->getContent( SlotRecord::MAIN );
3451  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3452 
3453  // Build rollback revision:
3454  // Restore old content
3455  // TODO: MCR: test this once we can store multiple slots
3456  foreach ( $target->getSlots()->getSlots() as $slot ) {
3457  $updater->inheritSlot( $slot );
3458  }
3459 
3460  // Remove extra slots
3461  // TODO: MCR: test this once we can store multiple slots
3462  foreach ( $current->getSlotRoles() as $role ) {
3463  if ( !$target->hasSlot( $role ) ) {
3464  $updater->removeSlot( $role );
3465  }
3466  }
3467 
3468  $updater->setOriginalRevisionId( $target->getId() );
3469  $oldestRevertedRevision = $this->getRevisionStore()->getNextRevision(
3470  $target,
3471  RevisionStore::READ_LATEST
3472  );
3473  if ( $oldestRevertedRevision !== null ) {
3474  $updater->markAsRevert(
3475  EditResult::REVERT_ROLLBACK,
3476  $oldestRevertedRevision->getId(),
3477  $current->getId()
3478  );
3479  }
3480 
3481  // TODO: this logic should not be in the storage layer, it's here for compatibility
3482  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3483  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3484 
3485  if ( $wgUseRCPatrol && $performer->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
3486  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3487  }
3488 
3489  // Actually store the rollback
3490  $rev = $updater->saveRevision(
3492  $flags
3493  );
3494 
3495  // Set patrolling and bot flag on the edits, which gets rollbacked.
3496  // This is done even on edit failure to have patrolling in that case (T64157).
3497  $set = [];
3498  if ( $bot && $performer->isAllowed( 'markbotedits' ) ) {
3499  // Mark all reverted edits as bot
3500  $set['rc_bot'] = 1;
3501  }
3502 
3503  if ( $wgUseRCPatrol ) {
3504  // Mark all reverted edits as patrolled
3505  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3506  }
3507 
3508  if ( count( $set ) ) {
3509  $actorWhere = ActorMigration::newMigration()->getWhere(
3510  $dbw,
3511  'rc_user',
3512  $current->getUser( RevisionRecord::RAW ),
3513  false
3514  );
3515  $dbw->update( 'recentchanges', $set,
3516  [ /* WHERE */
3517  'rc_cur_id' => $current->getPageId(),
3518  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3519  $actorWhere['conds'], // No tables/joins are needed for rc_user
3520  ],
3521  __METHOD__
3522  );
3523  }
3524 
3525  if ( !$updater->wasSuccessful() ) {
3526  return $updater->getStatus()->getErrorsArray();
3527  }
3528 
3529  // Report if the edit was not created because it did not change the content.
3530  if ( $updater->isUnchanged() ) {
3531  $resultDetails = new DeprecatablePropertyArray(
3532  [
3533  'current' => $legacyCurrentCallback,
3534  'current-revision-record' => $current,
3535  ],
3536  [ 'current' => '1.35' ],
3537  __METHOD__
3538  );
3539  return [ [ 'alreadyrolled',
3540  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3541  htmlspecialchars( $fromP ),
3542  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3543  ] ];
3544  }
3545 
3546  if ( $changingContentModel ) {
3547  // If the content model changed during the rollback,
3548  // make sure it gets logged to Special:Log/contentmodel
3549  $log = new ManualLogEntry( 'contentmodel', 'change' );
3550  $log->setPerformer( $performer->getPerformer() );
3551  $log->setTarget( $this->mTitle );
3552  $log->setComment( $summary );
3553  $log->setParameters( [
3554  '4::oldmodel' => $currentContent->getModel(),
3555  '5::newmodel' => $targetContent->getModel(),
3556  ] );
3557 
3558  $logId = $log->insert( $dbw );
3559  $log->publish( $logId );
3560  }
3561 
3562  $revId = $rev->getId();
3563 
3564  // Hook is hard deprecated since 1.35
3565  if ( $this->getHookContainer()->isRegistered( 'ArticleRollbackComplete' ) ) {
3566  // Only create the Revision objects if needed
3567  $legacyCurrent = new Revision( $current );
3568  $legacyTarget = new Revision( $target );
3569  $this->getHookRunner()->onArticleRollbackComplete( $this, $user,
3570  $legacyTarget, $legacyCurrent );
3571  }
3572 
3573  $this->getHookRunner()->onRollbackComplete( $this, $user, $target, $current );
3574 
3575  $legacyTargetCallback = static function () use ( $target ) {
3576  // Only create the Revision object if needed
3577  return new Revision( $target );
3578  };
3579 
3580  $tags = array_merge(
3581  $tags ?: [],
3582  $updater->getEditResult()->getRevertTags()
3583  );
3584 
3585  $resultDetails = new DeprecatablePropertyArray(
3586  [
3587  'summary' => $summary,
3588  'current' => $legacyCurrentCallback,
3589  'current-revision-record' => $current,
3590  'target' => $legacyTargetCallback,
3591  'target-revision-record' => $target,
3592  'newid' => $revId,
3593  'tags' => $tags
3594  ],
3595  [ 'current' => '1.35', 'target' => '1.35' ],
3596  __METHOD__
3597  );
3598 
3599  // TODO: make this return a Status object and wrap $resultDetails in that.
3600  return [];
3601  }
3602 
3614  public static function onArticleCreate( Title $title ) {
3615  // TODO: move this into a PageEventEmitter service
3616 
3617  // Update existence markers on article/talk tabs...
3618  $other = $title->getOtherPage();
3619 
3620  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3621  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3622 
3623  $title->touchLinks();
3624  $title->deleteTitleProtection();
3625 
3626  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3627 
3628  // Invalidate caches of articles which include this page
3630  $title,
3631  'templatelinks',
3632  [ 'causeAction' => 'page-create' ]
3633  );
3634  JobQueueGroup::singleton()->lazyPush( $job );
3635 
3636  if ( $title->getNamespace() === NS_CATEGORY ) {
3637  // Load the Category object, which will schedule a job to create
3638  // the category table row if necessary. Checking a replica DB is ok
3639  // here, in the worst case it'll run an unnecessary recount job on
3640  // a category that probably doesn't have many members.
3641  Category::newFromTitle( $title )->getID();
3642  }
3643  }
3644 
3650  public static function onArticleDelete( Title $title ) {
3651  // TODO: move this into a PageEventEmitter service
3652 
3653  // Update existence markers on article/talk tabs...
3654  $other = $title->getOtherPage();
3655 
3656  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3657  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3658 
3659  $title->touchLinks();
3660 
3661  $services = MediaWikiServices::getInstance();
3662  $services->getLinkCache()->invalidateTitle( $title );
3663 
3665 
3666  // Messages
3667  if ( $title->getNamespace() === NS_MEDIAWIKI ) {
3668  $services->getMessageCache()->updateMessageOverride( $title, null );
3669  }
3670 
3671  // Images
3672  if ( $title->getNamespace() === NS_FILE ) {
3674  $title,
3675  'imagelinks',
3676  [ 'causeAction' => 'page-delete' ]
3677  );
3678  JobQueueGroup::singleton()->lazyPush( $job );
3679  }
3680 
3681  // User talk pages
3682  if ( $title->getNamespace() === NS_USER_TALK ) {
3683  $user = User::newFromName( $title->getText(), false );
3684  if ( $user ) {
3685  MediaWikiServices::getInstance()
3686  ->getTalkPageNotificationManager()
3687  ->removeUserHasNewMessages( $user );
3688  }
3689  }
3690 
3691  // Image redirects
3692  $services->getRepoGroup()->getLocalRepo()->invalidateImageRedirect( $title );
3693 
3694  // Purge cross-wiki cache entities referencing this page
3696  }
3697 
3707  public static function onArticleEdit(
3708  Title $title,
3709  $revRecord = null,
3710  $slotsChanged = null
3711  ) {
3712  if ( $revRecord && $revRecord instanceof Revision ) {
3713  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
3714  $revRecord = $revRecord->getRevisionRecord();
3715  }
3716 
3717  // TODO: move this into a PageEventEmitter service
3718 
3719  $jobs = [];
3720  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3721  // Invalidate caches of articles which include this page.
3722  // Only for the main slot, because only the main slot is transcluded.
3723  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3725  $title,
3726  'templatelinks',
3727  [ 'causeAction' => 'page-edit' ]
3728  );
3729  }
3730  // Invalidate the caches of all pages which redirect here
3732  $title,
3733  'redirect',
3734  [ 'causeAction' => 'page-edit' ]
3735  );
3736  JobQueueGroup::singleton()->lazyPush( $jobs );
3737 
3738  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3739 
3740  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3741  $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3742 
3743  // Purge ?action=info cache
3744  $revid = $revRecord ? $revRecord->getId() : null;
3745  DeferredUpdates::addCallableUpdate( static function () use ( $title, $revid ) {
3747  } );
3748 
3749  // Purge cross-wiki cache entities referencing this page
3751  }
3752 
3760  private static function purgeInterwikiCheckKey( Title $title ) {
3762 
3763  if ( !$wgEnableScaryTranscluding ) {
3764  return; // @todo: perhaps this wiki is only used as a *source* for content?
3765  }
3766 
3767  DeferredUpdates::addCallableUpdate( static function () use ( $title ) {
3768  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3769  $cache->resetCheckKey(
3770  // Do not include the namespace since there can be multiple aliases to it
3771  // due to different namespace text definitions on different wikis. This only
3772  // means that some cache invalidations happen that are not strictly needed.
3773  $cache->makeGlobalKey(
3774  'interwiki-page',
3776  $title->getDBkey()
3777  )
3778  );
3779  } );
3780  }
3781 
3788  public function getCategories() {
3789  $id = $this->getId();
3790  if ( $id == 0 ) {
3791  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3792  }
3793 
3794  $dbr = wfGetDB( DB_REPLICA );
3795  $res = $dbr->select( 'categorylinks',
3796  [ 'page_title' => 'cl_to', 'page_namespace' => NS_CATEGORY ],
3797  [ 'cl_from' => $id ],
3798  __METHOD__
3799  );
3800 
3801  return TitleArray::newFromResult( $res );
3802  }
3803 
3810  public function getHiddenCategories() {
3811  $result = [];
3812  $id = $this->getId();
3813 
3814  if ( $id == 0 ) {
3815  return [];
3816  }
3817 
3818  $dbr = wfGetDB( DB_REPLICA );
3819  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3820  [ 'cl_to' ],
3821  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3822  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3823  __METHOD__ );
3824 
3825  if ( $res !== false ) {
3826  foreach ( $res as $row ) {
3827  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3828  }
3829  }
3830 
3831  return $result;
3832  }
3833 
3841  public function getAutoDeleteReason( &$hasHistory ) {
3842  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3843  }
3844 
3855  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3856  $id = $id ?: $this->getId();
3857  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3858  getCategoryLinkType( $this->getTitle()->getNamespace() );
3859 
3860  $addFields = [ 'cat_pages = cat_pages + 1' ];
3861  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3862  if ( $type !== 'page' ) {
3863  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3864  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3865  }
3866 
3867  $dbw = wfGetDB( DB_MASTER );
3868 
3869  if ( count( $added ) ) {
3870  $existingAdded = $dbw->selectFieldValues(
3871  'category',
3872  'cat_title',
3873  [ 'cat_title' => $added ],
3874  __METHOD__
3875  );
3876 
3877  // For category rows that already exist, do a plain
3878  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3879  // to avoid creating gaps in the cat_id sequence.
3880  if ( count( $existingAdded ) ) {
3881  $dbw->update(
3882  'category',
3883  $addFields,
3884  [ 'cat_title' => $existingAdded ],
3885  __METHOD__
3886  );
3887  }
3888 
3889  $missingAdded = array_diff( $added, $existingAdded );
3890  if ( count( $missingAdded ) ) {
3891  $insertRows = [];
3892  foreach ( $missingAdded as $cat ) {
3893  $insertRows[] = [
3894  'cat_title' => $cat,
3895  'cat_pages' => 1,
3896  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3897  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3898  ];
3899  }
3900  $dbw->upsert(
3901  'category',
3902  $insertRows,
3903  'cat_title',
3904  $addFields,
3905  __METHOD__
3906  );
3907  }
3908  }
3909 
3910  if ( count( $deleted ) ) {
3911  $dbw->update(
3912  'category',
3913  $removeFields,
3914  [ 'cat_title' => $deleted ],
3915  __METHOD__
3916  );
3917  }
3918 
3919  foreach ( $added as $catName ) {
3920  $cat = Category::newFromName( $catName );
3921  $this->getHookRunner()->onCategoryAfterPageAdded( $cat, $this );
3922  }
3923 
3924  foreach ( $deleted as $catName ) {
3925  $cat = Category::newFromName( $catName );
3926  $this->getHookRunner()->onCategoryAfterPageRemoved( $cat, $this, $id );
3927  // Refresh counts on categories that should be empty now (after commit, T166757)
3928  DeferredUpdates::addCallableUpdate( static function () use ( $cat ) {
3929  $cat->refreshCountsIfEmpty();
3930  } );
3931  }
3932  }
3933 
3940  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3941  if ( wfReadOnly() ) {
3942  return;
3943  }
3944 
3945  if ( !$this->getHookRunner()->onOpportunisticLinksUpdate( $this,
3946  $this->mTitle, $parserOutput )
3947  ) {
3948  return;
3949  }
3950 
3951  $config = RequestContext::getMain()->getConfig();
3952 
3953  $params = [
3954  'isOpportunistic' => true,
3955  'rootJobTimestamp' => $parserOutput->getCacheTime()
3956  ];
3957 
3958  if ( $this->mTitle->areRestrictionsCascading() ) {
3959  // If the page is cascade protecting, the links should really be up-to-date
3960  JobQueueGroup::singleton()->lazyPush(
3961  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3962  );
3963  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3964  // Assume the output contains "dynamic" time/random based magic words.
3965  // Only update pages that expired due to dynamic content and NOT due to edits
3966  // to referenced templates/files. When the cache expires due to dynamic content,
3967  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3968  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3969  // template/file edit already triggered recursive RefreshLinksJob jobs.
3970  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3971  // If a page is uncacheable, do not keep spamming a job for it.
3972  // Although it would be de-duplicated, it would still waste I/O.
3974  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3975  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3976  if ( $cache->add( $key, time(), $ttl ) ) {
3977  JobQueueGroup::singleton()->lazyPush(
3978  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3979  );
3980  }
3981  }
3982  }
3983  }
3984 
3994  public function getDeletionUpdates( $rev = null ) {
3995  if ( !$rev ) {
3996  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3997 
3998  try {
3999  $rev = $this->getRevisionRecord();
4000  } catch ( Exception $ex ) {
4001  // If we can't load the content, something is wrong. Perhaps that's why
4002  // the user is trying to delete the page, so let's not fail in that case.
4003  // Note that doDeleteArticleReal() will already have logged an issue with
4004  // loading the content.
4005  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
4006  }
4007  }
4008 
4009  if ( !$rev ) {
4010  $slotContent = [];
4011  } elseif ( $rev instanceof Content ) {
4012  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
4013 
4014  $slotContent = [ SlotRecord::MAIN => $rev ];
4015  } else {
4016  $slotContent = array_map( static function ( SlotRecord $slot ) {
4017  return $slot->getContent();
4018  }, $rev->getSlots()->getSlots() );
4019  }
4020 
4021  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
4022 
4023  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
4024  // model here, not the content object!
4025  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
4027  $content = null; // in case $slotContent is zero-length
4028  foreach ( $slotContent as $role => $content ) {
4029  $handler = $content->getContentHandler();
4030 
4031  $updates = $handler->getDeletionUpdates(
4032  $this->getTitle(),
4033  $role
4034  );
4035  $allUpdates = array_merge( $allUpdates, $updates );
4036 
4037  // TODO: remove B/C hack in 1.32!
4038  $legacyUpdates = $content->getDeletionUpdates( $this );
4039 
4040  // HACK: filter out redundant and incomplete LinksDeletionUpdate
4041  $legacyUpdates = array_filter( $legacyUpdates, static function ( $update ) {
4042  return !( $update instanceof LinksDeletionUpdate );
4043  } );
4044 
4045  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
4046  }
4047 
4048  $this->getHookRunner()->onPageDeletionDataUpdates(
4049  $this->getTitle(), $rev, $allUpdates );
4050 
4051  // TODO: hard deprecate old hook in 1.33
4052  $this->getHookRunner()->onWikiPageDeletionUpdates( $this, $content, $allUpdates );
4053  return $allUpdates;
4054  }
4055 
4063  public function isLocal() {
4064  return true;
4065  }
4066 
4076  public function getWikiDisplayName() {
4077  global $wgSitename;
4078  return $wgSitename;
4079  }
4080 
4089  public function getSourceURL() {
4090  return $this->getTitle()->getCanonicalURL();
4091  }
4092 
4099  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
4100 
4101  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
4102  }
4103 
4110  public function __wakeup() {
4111  // Make sure we re-fetch the latest state from the database.
4112  // In particular, the latest revision may have changed.
4113  // As a side-effect, this makes sure mLastRevision doesn't
4114  // end up being an instance of the old Revision class (see T259181).
4115  $this->clear();
4116  }
4117 
4122  public function getNamespace(): int {
4123  return $this->getTitle()->getNamespace();
4124  }
4125 
4130  public function getDBkey(): string {
4131  return $this->getTitle()->getDBkey();
4132  }
4133 
4138  public function getWikiId() {
4139  return $this->getTitle()->getWikiId();
4140  }
4141 
4146  public function canExist(): bool {
4147  // NOTE: once WikiPage becomes a ProperPageIdentity, this should always return true
4148  return $this->mTitle->canExist();
4149  }
4150 
4155  public function __toString(): string {
4156  return $this->mTitle->__toString();
4157  }
4158 
4166  public function isSamePageAs( PageIdentity $other ): bool {
4167  // NOTE: keep in sync with PageIdentityValue::isSamePageAs()!
4168 
4169  if ( $other->getWikiId() !== $this->getWikiId()
4170  || $other->getId() !== $this->getId() ) {
4171  return false;
4172  }
4173 
4174  if ( $this->getId() === 0 ) {
4175  if ( $other->getNamespace() !== $this->getNamespace()
4176  || $other->getDBkey() !== $this->getDBkey() ) {
4177  return false;
4178  }
4179  }
4180 
4181  return true;
4182  }
4183 
4184 }
WikiPage\hasDifferencesOutsideMainSlot
static hasDifferencesOutsideMainSlot( $a, $b)
Helper method for checking whether two revisions have differences that go beyond the main slot.
Definition: WikiPage.php:1570
WikiPage\getCategories
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:3788
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:65
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:44
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:138
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:623
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:2036
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:3614
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
Page\PageIdentity\getDBkey
getDBkey()
Get the page title in DB key form.
WikiPage\loadPageData
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:459
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
WikiPage\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3841
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:829
ParserOutput
Definition: ParserOutput.php:31
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:295
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:1035
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:950
WikiPage\getWikiId
getWikiId()
Definition: WikiPage.php:4138
WikiPage\clearCacheFields
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:327
Title\getFragment
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1775
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:2792
WikiPage\updateRevisionOn
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1402
WikiPage\wasLoadedFrom
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:516
TitleArray\newFromResult
static newFromResult( $res)
Definition: TitleArray.php:42
WikiPage\getUndoContent
getUndoContent(Revision $undo, Revision $undoafter)
Get the content that needs to be saved in order to undo all revisions between $undo and $undoafter.
Definition: WikiPage.php:1599
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1271
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:172
MediaWiki\Storage\DerivedPageDataUpdater\setArticleCountMethod
setArticleCountMethod( $articleCountMethod)
Definition: DerivedPageDataUpdater.php:451
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(Title $title, ?RevisionRecord $old, ?RevisionRecord $new, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:547
Page\ParserOutputAccess
Service for getting rendered output of a given page.
Definition: ParserOutputAccess.php:49
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:84
WikiPage\newPageUpdater
newPageUpdater(User $user, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
Definition: WikiPage.php:1864
WikiPage\hasViewableContent
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition: WikiPage.php:632
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:709
WikiPage\__toString
__toString()
Returns an informative human readable representation of the page identity, for use in logging and deb...
Definition: WikiPage.php:4155
WikiPage\doViewUpdates
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1297
MediaWiki\Storage\DerivedPageDataUpdater\setRcWatchCategoryMembership
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
Definition: DerivedPageDataUpdater.php:459
WikiPage\getUserText
getUserText( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:929
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1685
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1731
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:449
SearchUpdate
Database independent search index updater.
Definition: SearchUpdate.php:33
WikiPage\$mDataLoadedFrom
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:111
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:61
WikiPage\replaceSectionContent
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1647
Page\PageIdentity\getId
getId( $wikiId=self::LOCAL)
Returns the page ID.
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2148
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1159
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1135
wfMsgReplaceArgs
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
Definition: GlobalFunctions.php:1266
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:584
WikiPage\getRevision
getRevision()
Get the latest revision.
Definition: WikiPage.php:815
RefreshLinksJob\newDynamic
static newDynamic(Title $title, array $params)
Definition: RefreshLinksJob.php:80
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1231
RefreshLinksJob\newPrioritized
static newPrioritized(Title $title, array $params)
Definition: RefreshLinksJob.php:68
Revision\getContentHandler
getContentHandler()
Returns the content handler appropriate for this revision's content model.
Definition: Revision.php:863
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:1197
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:655
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1094
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
$success
$success
Definition: NoLocalSettings.php:42
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
Definition: FakeResultWrapper.php:11
WikiPage\archiveRevisions
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:3038
WikiPage\getSlotRoleRegistry
getSlotRoleRegistry()
Definition: WikiPage.php:255
WikiPage\getDBLoadBalancer
getDBLoadBalancer()
Definition: WikiPage.php:276
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:286
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:7433
$revQuery
$revQuery
Definition: testCompression.php:56
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:7449
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:34
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:157
HTMLCacheUpdateJob\newForBacklinks
static newForBacklinks(Title $title, $table, $params=[])
Definition: HTMLCacheUpdateJob.php:59
WikiPage\$mTitle
Title $mTitle
Definition: WikiPage.php:73
MediaWiki\Permissions\Authority\isAllowedAny
isAllowedAny(... $permissions)
Checks whether this authority has any of the given permissions in general.
WikiPage\$mTouched
string $mTouched
Definition: WikiPage.php:131
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 given fresh parser output if useful.
Definition: WikiPage.php:3940
WikiPage\protectDescription
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2724
Title\castFromPageIdentity
static castFromPageIdentity(?PageIdentity $pageIdentity)
Return a Title for a given PageIdentity.
Definition: Title.php:328
WikiPage\$mLastRevision
RevisionRecord $mLastRevision
Definition: WikiPage.php:121
$dbr
$dbr
Definition: testCompression.php:54
Revision
Definition: Revision.php:40
WikiPage\updateParserCache
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2290
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1629
$wgEnableScaryTranscluding
$wgEnableScaryTranscluding
Enable interwiki transcluding.
Definition: DefaultSettings.php:4834
MediaWiki\Permissions\Authority\getPerformer
getPerformer()
Returns the performer of the actions associated with this authority.
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:1059
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1066
MWException
MediaWiki exception.
Definition: MWException.php:29
WikiPage\getDBkey
getDBkey()
Get the page title in DB key form.When getId() would throw, the behavior of this method is undefined....
Definition: WikiPage.php:4130
WikiPage\doDeleteArticleBatched
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2866
WikiPage\assertProperPage
assertProperPage()
Code that requires this WikiPage to be a "proper page" in the sense defined by PageIdentity should ca...
Definition: WikiPage.php:592
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:965
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:27
WikiPage\isSamePageAs
isSamePageAs(PageIdentity $other)
Checks whether the given PageIdentity refers to the same page as this PageIdentity....
Definition: WikiPage.php:4166
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1034
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
MediaWiki\Storage\DerivedPageDataUpdater\setLogger
setLogger(LoggerInterface $logger)
Definition: DerivedPageDataUpdater.php:338
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1068
WikiPage\doSecondaryDataUpdates
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2334
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2467
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:347
Title\getInterwiki
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:969
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1355
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1248
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:308
MediaWiki\Content\ContentHandlerFactory
Definition: ContentHandlerFactory.php:44
WikiPage\exists
exists()
Definition: WikiPage.php:617
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:168
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3650
ObjectCache\getInstance
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:74
$args
if( $line===false) $args
Definition: mcc.php:124
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:136
WikiPage\$mRedirectTarget
Title $mRedirectTarget
Definition: WikiPage.php:116
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:698
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:720
WikiPage\purgeInterwikiCheckKey
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3760
WikiPage\$mDataLoaded
bool $mDataLoaded
Definition: WikiPage.php:80
ChangeTags\getSoftwareTags
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:140
MediaWiki\User\UserIdentity\getName
getName()
$title
$title
Definition: testCompression.php:38
SiteStatsUpdate\factory
static factory(array $deltas)
Definition: SiteStatsUpdate.php:71
WikiPage\doDeleteUpdates
doDeleteUpdates( $id, Content $content=null, $revRecord=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:3183
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:626
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:875
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:731
MediaWiki\Permissions\Authority\authorizeWrite
authorizeWrite(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize write access.
User\newFromAnyId
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:682
User\getId
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition: User.php:2067
DB_MASTER
const DB_MASTER
Definition: defines.php:26
$revStore
$revStore
Definition: testCompression.php:55
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:94
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:915
WikiPage\doPurge
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1320
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:982
User\clearNotification
clearNotification(&$title, $oldid=0)
Clear the user's notification timestamp for the given title.
Definition: User.php:3263
WikiPage\getContentModel
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:659
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:425
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:3154
Wikimedia\Rdbms\LoadBalancer
Database connection, tracking, load balancing, and transaction manager for a cluster.
Definition: LoadBalancer.php:42
$wgPageLanguageUseDB
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
Definition: DefaultSettings.php:9286
Category\newFromTitle
static newFromTitle( $title)
Factory function.
Definition: Category.php:153
MediaWiki\Permissions\Authority
@unstable
Definition: Authority.php:30
WikiPage\getDerivedDataUpdater
getDerivedDataUpdater(User $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:1808
WikiPage\updateIfNewerOn
updateIfNewerOn( $dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record.
Definition: WikiPage.php:1520
MediaWiki\Storage\RevisionSlotsUpdate
Value object representing a modification of revision slots.
Definition: RevisionSlotsUpdate.php:36
Revision\RevisionRenderer
The RevisionRenderer service provides access to rendered output for revisions.
Definition: RevisionRenderer.php:45
WikiPage\getId
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:604
$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:3994
MediaWiki\DAO\WikiAwareEntity\assertWiki
assertWiki( $wikiId)
Throws if $wikiId is different from the return value of getWikiId().
WikiPage\doRollback
doRollback( $fromP, $summary, $token, $bot, &$resultDetails, Authority $performer, $tags=null)
Roll back the most recent consecutive set of edits to a page from the same user; fails if there are n...
Definition: WikiPage.php:3272
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:188
WikiPage\protectDescriptionLog
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2766
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:1082
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition: WikiPage.php:850
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
MediaWiki\Storage\EditResultCache
Class allowing easy storage and retrieval of EditResults associated with revisions.
Definition: EditResultCache.php:42
WikiPage\commitRollback
commitRollback( $fromP, $summary, $bot, &$resultDetails, Authority $performer, $tags=null)
Backend implementation of doRollback(), please refer there for parameter and return value documentati...
Definition: WikiPage.php:3323
WikiPage\doEditUpdates
doEditUpdates( $revisionRecord, User $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2251
MediaWiki\Storage\PageUpdater
Controller-like object for creating and updating pages by creating new revisions.
Definition: PageUpdater.php:77
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:125
WikiPage\newFromID
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:198
$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:4089
ParserOptions\newCanonical
static newCanonical( $context=null, $userLang=null)
Creates a "canonical" ParserOptions object.
Definition: ParserOptions.php:1178
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1505
WikiPage\$derivedDataUpdater
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:141
LinksDeletionUpdate
Update object handling the cleanup of links tables after a page was deleted.
Definition: LinksDeletionUpdate.php:28
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:1961
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3810
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:214
WikiPage\canExist
canExist()
Definition: WikiPage.php:4146
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:85
$wgDeleteRevisionsBatchSize
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
Definition: DefaultSettings.php:6063
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:476
WikiPage\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object.
Definition: WikiPage.php:360
WikiPage\getOldestRevision
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:744
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:754
WikiPage\setLastEdit
setLastEdit(RevisionRecord $revRecord)
Set the latest revision.
Definition: WikiPage.php:805
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:2642
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1148
MediaWiki\Permissions\PermissionStatus
A StatusValue for permission errors.
Definition: PermissionStatus.php:34
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:137
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:542
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:5857
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2702
Title
Represents a title within MediaWiki.
Definition: Title.php:46
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:260
MediaWiki\Permissions\Authority\isAllowed
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
WikiPage\$mIsRedirect
bool $mIsRedirect
Definition: WikiPage.php:87
WikiPage\newDerivedDataUpdater
newDerivedDataUpdater()
Definition: WikiPage.php:1746
JobQueueGroup\singleton
static singleton( $domain=false)
Definition: JobQueueGroup.php:70
wfReadOnlyReason
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
Definition: GlobalFunctions.php:1148
WikiPage\getUser
getUser( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: WikiPage.php:889
$cache
$cache
Definition: mcc.php:33
WikiPage\factory
static factory(PageIdentity $pageIdentity)
Create a WikiPage object of the appropriate class for the given PageIdentity.
Definition: WikiPage.php:183
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:50
WebRequest\getRequestId
static getRequestId()
Get the current request ID.
Definition: WebRequest.php:330
WikiPage\$mId
int $mId
Definition: WikiPage.php:106
DeletePageJob
Class DeletePageJob.
Definition: DeletePageJob.php:8
WikiPage\getWikiDisplayName
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:4076
LinksUpdate\queueRecursiveJobsForTable
static queueRecursiveJobsForTable(Title $title, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
Definition: LinksUpdate.php:369
WikiPage\prepareContentForEdit
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:2179
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:161
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:224
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:3855
Page\PageIdentity\getWikiId
getWikiId()
Get the ID of the wiki this page belongs to.
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:4098
InfoAction\invalidateCache
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:71
$source
$source
Definition: mwdoc-filter.php:34
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:43
WikiPage\pageData
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:396
Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
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:4063
$wgArticleCountMethod
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article.
Definition: DefaultSettings.php:4876
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:1108
NS_FILE
const NS_FILE
Definition: Defines.php:70
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:861
WikiPage\getRevisionRenderer
getRevisionRenderer()
Definition: WikiPage.php:248
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1483
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:140
WikiPage\$mPreparedEdit
PreparedEdit false $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:101
Revision\getRevisionRecord
getRevisionRecord()
Definition: Revision.php:438
WikiPage\$mLinksUpdated
string $mLinksUpdated
Definition: WikiPage.php:136
WikiPage\onArticleEdit
static onArticleEdit(Title $title, $revRecord=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3707
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:641
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:138
WikiPage\__construct
__construct(PageIdentity $pageIdentity)
Definition: WikiPage.php:146
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:104
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:65
CommentStore\getStore
static getStore()
Definition: CommentStore.php:120
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:66
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
ParserOutput\hasDynamicContent
hasDynamicContent()
Check whether the cache TTL was lowered due to dynamic content.
Definition: ParserOutput.php:1340
WikiPage\getParserCache
getParserCache()
Definition: WikiPage.php:269
WikiPage\getCreator
getCreator( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:910
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:443
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:299
WikiPage\__wakeup
__wakeup()
Ensure consistency when unserializing.
Definition: WikiPage.php:4110
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2108
Page\PageIdentity\getNamespace
getNamespace()
Returns the page's namespace number.
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:126
MediaWiki\Debug\DeprecatablePropertyArray
ArrayAccess implementation that supports deprecating access to certain properties.
Definition: DeprecatablePropertyArray.php:16
Title\purgeExpiredRestrictions
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3008
WikiPage\getNamespace
getNamespace()
Returns the page's namespace number.The value returned by this method should represent a valid namesp...
Definition: WikiPage.php:4122
WikiPage\doDeleteArticleReal
doDeleteArticleReal( $reason, User $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:2827
$wgDisableAnonTalk
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
Definition: DefaultSettings.php:7546
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
$wgRCWatchCategoryMembership
$wgRCWatchCategoryMembership
Treat category membership changes as a RecentChange.
Definition: DefaultSettings.php:7423
WikiPage\getContentHandlerFactory
getContentHandlerFactory()
Definition: WikiPage.php:262
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:316
WikiPage\doUpdateRestrictions
doUpdateRestrictions(array $limit, array $expiry, &$cascade, $reason, User $user, $tags=null)
Update the article's restriction field, and leave a log entry.
Definition: WikiPage.php:2364
WikiPage\getRevisionStore
getRevisionStore()
Definition: WikiPage.php:241
$type
$type
Definition: testCompression.php:52
EDIT_INTERNAL
const EDIT_INTERNAL
Definition: Defines.php:143