MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
27 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
39 use Wikimedia\Assert\Assert;
40 use Wikimedia\IPUtils;
44 
51 class WikiPage implements Page, IDBAccessObject {
52  use ProtectedHookAccessorTrait;
53 
54  // Constants for $mDataLoadedFrom and related
55 
61  public $mTitle = null;
62 
68  public $mDataLoaded = false;
69 
75  public $mIsRedirect = false;
76 
82  public $mLatest = false;
83 
89  public $mPreparedEdit = false;
90 
94  protected $mId = null;
95 
100 
104  protected $mRedirectTarget = null;
105 
109  private $mLastRevision = null;
110 
114  protected $mTimestamp = '';
115 
119  protected $mTouched = '19700101000000';
120 
124  protected $mLinksUpdated = '19700101000000';
125 
129  private $derivedDataUpdater = null;
130 
134  public function __construct( Title $title ) {
135  $this->mTitle = $title;
136  }
137 
142  public function __clone() {
143  $this->mTitle = clone $this->mTitle;
144  }
145 
154  public static function factory( Title $title ) {
155  $ns = $title->getNamespace();
156 
157  if ( $ns == NS_MEDIA ) {
158  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
159  } elseif ( $ns < 0 ) {
160  throw new MWException( "Invalid or virtual namespace $ns given." );
161  }
162 
163  $page = null;
164  if ( !Hooks::runner()->onWikiPageFactory( $title, $page ) ) {
165  return $page;
166  }
167 
168  switch ( $ns ) {
169  case NS_FILE:
170  $page = new WikiFilePage( $title );
171  break;
172  case NS_CATEGORY:
173  $page = new WikiCategoryPage( $title );
174  break;
175  default:
176  $page = new WikiPage( $title );
177  }
178 
179  return $page;
180  }
181 
192  public static function newFromID( $id, $from = 'fromdb' ) {
193  // page ids are never 0 or negative, see T63166
194  if ( $id < 1 ) {
195  return null;
196  }
197 
198  $from = self::convertSelectType( $from );
199  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
200  $pageQuery = self::getQueryInfo();
201  $row = $db->selectRow(
202  $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
203  [], $pageQuery['joins']
204  );
205  if ( !$row ) {
206  return null;
207  }
208  return self::newFromRow( $row, $from );
209  }
210 
222  public static function newFromRow( $row, $from = 'fromdb' ) {
223  $page = self::factory( Title::newFromRow( $row ) );
224  $page->loadFromRow( $row, $from );
225  return $page;
226  }
227 
234  protected static function convertSelectType( $type ) {
235  switch ( $type ) {
236  case 'fromdb':
237  return self::READ_NORMAL;
238  case 'fromdbmaster':
239  return self::READ_LATEST;
240  case 'forupdate':
241  return self::READ_LOCKING;
242  default:
243  // It may already be an integer or whatever else
244  return $type;
245  }
246  }
247 
251  private function getRevisionStore() {
252  return MediaWikiServices::getInstance()->getRevisionStore();
253  }
254 
258  private function getRevisionRenderer() {
259  return MediaWikiServices::getInstance()->getRevisionRenderer();
260  }
261 
265  private function getSlotRoleRegistry() {
266  return MediaWikiServices::getInstance()->getSlotRoleRegistry();
267  }
268 
273  return MediaWikiServices::getInstance()->getContentHandlerFactory();
274  }
275 
279  private function getParserCache() {
280  return MediaWikiServices::getInstance()->getParserCache();
281  }
282 
286  private function getDBLoadBalancer() {
287  return MediaWikiServices::getInstance()->getDBLoadBalancer();
288  }
289 
296  public function getActionOverrides() {
297  return $this->getContentHandler()->getActionOverrides();
298  }
299 
309  public function getContentHandler() {
310  return $this->getContentHandlerFactory()
311  ->getContentHandler( $this->getContentModel() );
312  }
313 
318  public function getTitle() {
319  return $this->mTitle;
320  }
321 
326  public function clear() {
327  $this->mDataLoaded = false;
328  $this->mDataLoadedFrom = self::READ_NONE;
329 
330  $this->clearCacheFields();
331  }
332 
337  protected function clearCacheFields() {
338  $this->mId = null;
339  $this->mRedirectTarget = null; // Title object if set
340  $this->mLastRevision = null; // Latest revision
341  $this->mTouched = '19700101000000';
342  $this->mLinksUpdated = '19700101000000';
343  $this->mTimestamp = '';
344  $this->mIsRedirect = false;
345  $this->mLatest = false;
346  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
347  // checks the requested rev ID and content against the cached one. For most
348  // content types, the output should not change during the lifetime of this cache.
349  // Clearing it can cause extra parses on edit for no reason.
350  }
351 
357  public function clearPreparedEdit() {
358  $this->mPreparedEdit = false;
359  }
360 
370  public static function getQueryInfo() {
371  global $wgPageLanguageUseDB;
372 
373  $ret = [
374  'tables' => [ 'page' ],
375  'fields' => [
376  'page_id',
377  'page_namespace',
378  'page_title',
379  'page_restrictions',
380  'page_is_redirect',
381  'page_is_new',
382  'page_random',
383  'page_touched',
384  'page_links_updated',
385  'page_latest',
386  'page_len',
387  'page_content_model',
388  ],
389  'joins' => [],
390  ];
391 
392  if ( $wgPageLanguageUseDB ) {
393  $ret['fields'][] = 'page_lang';
394  }
395 
396  return $ret;
397  }
398 
406  protected function pageData( $dbr, $conditions, $options = [] ) {
407  $pageQuery = self::getQueryInfo();
408 
409  $this->getHookRunner()->onArticlePageDataBefore(
410  $this, $pageQuery['fields'], $pageQuery['tables'], $pageQuery['joins'] );
411 
412  $row = $dbr->selectRow(
413  $pageQuery['tables'],
414  $pageQuery['fields'],
415  $conditions,
416  __METHOD__,
417  $options,
418  $pageQuery['joins']
419  );
420 
421  $this->getHookRunner()->onArticlePageDataAfter( $this, $row );
422 
423  return $row;
424  }
425 
435  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
436  return $this->pageData( $dbr, [
437  'page_namespace' => $title->getNamespace(),
438  'page_title' => $title->getDBkey() ], $options );
439  }
440 
449  public function pageDataFromId( $dbr, $id, $options = [] ) {
450  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
451  }
452 
465  public function loadPageData( $from = 'fromdb' ) {
466  $from = self::convertSelectType( $from );
467  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
468  // We already have the data from the correct location, no need to load it twice.
469  return;
470  }
471 
472  if ( is_int( $from ) ) {
473  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
474  $loadBalancer = $this->getDBLoadBalancer();
475  $db = $loadBalancer->getConnection( $index );
476  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
477 
478  if ( !$data
479  && $index == DB_REPLICA
480  && $loadBalancer->getServerCount() > 1
481  && $loadBalancer->hasOrMadeRecentMasterChanges()
482  ) {
483  $from = self::READ_LATEST;
484  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
485  $db = $loadBalancer->getConnection( $index );
486  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
487  }
488  } else {
489  // No idea from where the caller got this data, assume replica DB.
490  $data = $from;
491  $from = self::READ_NORMAL;
492  }
493 
494  $this->loadFromRow( $data, $from );
495  }
496 
510  public function wasLoadedFrom( $from ) {
511  $from = self::convertSelectType( $from );
512 
513  if ( !is_int( $from ) ) {
514  // No idea from where the caller got this data, assume replica DB.
515  $from = self::READ_NORMAL;
516  }
517 
518  if ( $from <= $this->mDataLoadedFrom ) {
519  return true;
520  }
521 
522  return false;
523  }
524 
536  public function loadFromRow( $data, $from ) {
537  $lc = MediaWikiServices::getInstance()->getLinkCache();
538  $lc->clearLink( $this->mTitle );
539 
540  if ( $data ) {
541  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
542 
543  $this->mTitle->loadFromRow( $data );
544 
545  // Old-fashioned restrictions
546  $this->mTitle->loadRestrictions( $data->page_restrictions );
547 
548  $this->mId = intval( $data->page_id );
549  $this->mTouched = MWTimestamp::convert( TS_MW, $data->page_touched );
550  $this->mLinksUpdated = $data->page_links_updated === null
551  ? null
552  : MWTimestamp::convert( TS_MW, $data->page_links_updated );
553  $this->mIsRedirect = intval( $data->page_is_redirect );
554  $this->mLatest = intval( $data->page_latest );
555  // T39225: $latest may no longer match the cached latest RevisionRecord object.
556  // Double-check the ID of any cached latest RevisionRecord object for consistency.
557  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
558  $this->mLastRevision = null;
559  $this->mTimestamp = '';
560  }
561  } else {
562  $lc->addBadLinkObj( $this->mTitle );
563 
564  $this->mTitle->loadFromRow( false );
565 
566  $this->clearCacheFields();
567 
568  $this->mId = 0;
569  }
570 
571  $this->mDataLoaded = true;
572  $this->mDataLoadedFrom = self::convertSelectType( $from );
573  }
574 
578  public function getId() {
579  if ( !$this->mDataLoaded ) {
580  $this->loadPageData();
581  }
582  return $this->mId;
583  }
584 
588  public function exists() {
589  if ( !$this->mDataLoaded ) {
590  $this->loadPageData();
591  }
592  return $this->mId > 0;
593  }
594 
603  public function hasViewableContent() {
604  return $this->mTitle->isKnown();
605  }
606 
612  public function isRedirect() {
613  if ( !$this->mDataLoaded ) {
614  $this->loadPageData();
615  }
616 
617  return (bool)$this->mIsRedirect;
618  }
619 
630  public function getContentModel() {
631  if ( $this->exists() ) {
632  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
633 
634  return $cache->getWithSetCallback(
635  $cache->makeKey( 'page-content-model', $this->getLatest() ),
636  $cache::TTL_MONTH,
637  function () {
638  $rev = $this->getRevisionRecord();
639  if ( $rev ) {
640  // Look at the revision's actual content model
641  $slot = $rev->getSlot(
642  SlotRecord::MAIN,
643  RevisionRecord::RAW
644  );
645  return $slot->getModel();
646  } else {
647  $title = $this->mTitle->getPrefixedDBkey();
648  wfWarn( "Page $title exists but has no (visible) revisions!" );
649  return $this->mTitle->getContentModel();
650  }
651  }
652  );
653  }
654 
655  // use the default model for this page
656  return $this->mTitle->getContentModel();
657  }
658 
663  public function checkTouched() {
664  if ( !$this->mDataLoaded ) {
665  $this->loadPageData();
666  }
667  return ( $this->mId && !$this->mIsRedirect );
668  }
669 
674  public function getTouched() {
675  if ( !$this->mDataLoaded ) {
676  $this->loadPageData();
677  }
678  return $this->mTouched;
679  }
680 
685  public function getLinksTimestamp() {
686  if ( !$this->mDataLoaded ) {
687  $this->loadPageData();
688  }
689  return $this->mLinksUpdated;
690  }
691 
696  public function getLatest() {
697  if ( !$this->mDataLoaded ) {
698  $this->loadPageData();
699  }
700  return (int)$this->mLatest;
701  }
702 
709  public function getOldestRevision() {
710  wfDeprecated( __METHOD__, '1.35' );
711  $rev = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
712  return $rev ? new Revision( $rev ) : null;
713  }
714 
719  protected function loadLastEdit() {
720  if ( $this->mLastRevision !== null ) {
721  return; // already loaded
722  }
723 
724  $latest = $this->getLatest();
725  if ( !$latest ) {
726  return; // page doesn't exist or is missing page_latest info
727  }
728 
729  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
730  // T39225: if session S1 loads the page row FOR UPDATE, the result always
731  // includes the latest changes committed. This is true even within REPEATABLE-READ
732  // transactions, where S1 normally only sees changes committed before the first S1
733  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
734  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
735  // happened after the first S1 SELECT.
736  // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
737  $revision = $this->getRevisionStore()
738  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LOCKING );
739  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
740  // Bug T93976: if page_latest was loaded from the master, fetch the
741  // revision from there as well, as it may not exist yet on a replica DB.
742  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
743  $revision = $this->getRevisionStore()
744  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LATEST );
745  } else {
746  $revision = $this->getRevisionStore()->getKnownCurrentRevision( $this->getTitle(), $latest );
747  }
748 
749  if ( $revision ) { // sanity
750  $this->setLastEdit( $revision );
751  }
752  }
753 
758  private function setLastEdit( RevisionRecord $revRecord ) {
759  $this->mLastRevision = $revRecord;
760  $this->mTimestamp = $revRecord->getTimestamp();
761  }
762 
768  public function getRevision() {
769  wfDeprecated( __METHOD__, '1.35' );
770  $this->loadLastEdit();
771  if ( $this->mLastRevision ) {
772  return new Revision( $this->mLastRevision );
773  }
774  return null;
775  }
776 
781  public function getRevisionRecord() {
782  $this->loadLastEdit();
783  if ( $this->mLastRevision ) {
784  return $this->mLastRevision;
785  }
786  return null;
787  }
788 
802  public function getContent( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
803  $this->loadLastEdit();
804  if ( $this->mLastRevision ) {
805  return $this->mLastRevision->getContent( SlotRecord::MAIN, $audience, $user );
806  }
807  return null;
808  }
809 
813  public function getTimestamp() {
814  // Check if the field has been filled by WikiPage::setTimestamp()
815  if ( !$this->mTimestamp ) {
816  $this->loadLastEdit();
817  }
818 
819  return MWTimestamp::convert( TS_MW, $this->mTimestamp );
820  }
821 
827  public function setTimestamp( $ts ) {
828  $this->mTimestamp = MWTimestamp::convert( TS_MW, $ts );
829  }
830 
840  public function getUser( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
841  $this->loadLastEdit();
842  if ( $this->mLastRevision ) {
843  if ( $audience === RevisionRecord::FOR_THIS_USER && $user === null ) {
844  wfDeprecated(
845  __METHOD__ . ' using FOR_THIS_USER without a user',
846  '1.35'
847  );
848  global $wgUser;
849  $user = $wgUser;
850  }
851  $revUser = $this->mLastRevision->getUser( $audience, $user );
852  return $revUser ? $revUser->getId() : 0;
853  } else {
854  return -1;
855  }
856  }
857 
868  public function getCreator( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
869  $revRecord = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
870  if ( $revRecord ) {
871  if ( $audience === RevisionRecord::FOR_THIS_USER && $user === null ) {
872  wfDeprecated(
873  __METHOD__ . ' using FOR_THIS_USER without a user',
874  '1.35'
875  );
876  global $wgUser;
877  $user = $wgUser;
878  }
879  return $revRecord->getUser( $audience, $user );
880  } else {
881  return null;
882  }
883  }
884 
894  public function getUserText( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
895  $this->loadLastEdit();
896  if ( $this->mLastRevision ) {
897  if ( $audience === RevisionRecord::FOR_THIS_USER && $user === null ) {
898  wfDeprecated(
899  __METHOD__ . ' using FOR_THIS_USER without a user',
900  '1.35'
901  );
902  global $wgUser;
903  $user = $wgUser;
904  }
905  $revUser = $this->mLastRevision->getUser( $audience, $user );
906  return $revUser ? $revUser->getName() : '';
907  } else {
908  return '';
909  }
910  }
911 
922  public function getComment( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
923  $this->loadLastEdit();
924  if ( $this->mLastRevision ) {
925  if ( $audience === RevisionRecord::FOR_THIS_USER && $user === null ) {
926  wfDeprecated(
927  __METHOD__ . ' using FOR_THIS_USER without a user',
928  '1.35'
929  );
930  global $wgUser;
931  $user = $wgUser;
932  }
933  $revComment = $this->mLastRevision->getComment( $audience, $user );
934  return $revComment ? $revComment->text : '';
935  } else {
936  return '';
937  }
938  }
939 
945  public function getMinorEdit() {
946  $this->loadLastEdit();
947  if ( $this->mLastRevision ) {
948  return $this->mLastRevision->isMinor();
949  } else {
950  return false;
951  }
952  }
953 
962  public function isCountable( $editInfo = false ) {
963  global $wgArticleCountMethod;
964 
965  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
966 
967  if ( !$this->mTitle->isContentPage() ) {
968  return false;
969  }
970 
971  if ( $editInfo ) {
972  // NOTE: only the main slot can make a page a redirect
973  $content = $editInfo->pstContent;
974  } else {
975  $content = $this->getContent();
976  }
977 
978  if ( !$content || $content->isRedirect() ) {
979  return false;
980  }
981 
982  $hasLinks = null;
983 
984  if ( $wgArticleCountMethod === 'link' ) {
985  // nasty special case to avoid re-parsing to detect links
986 
987  if ( $editInfo ) {
988  // ParserOutput::getLinks() is a 2D array of page links, so
989  // to be really correct we would need to recurse in the array
990  // but the main array should only have items in it if there are
991  // links.
992  $hasLinks = (bool)count( $editInfo->output->getLinks() );
993  } else {
994  // NOTE: keep in sync with RevisionRenderer::getLinkCount
995  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
996  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', '1',
997  [ 'pl_from' => $this->getId() ], __METHOD__ );
998  }
999  }
1000 
1001  // TODO: MCR: determine $hasLinks for each slot, and use that info
1002  // with that slot's Content's isCountable method. That requires per-
1003  // slot ParserOutput in the ParserCache, or per-slot info in the
1004  // pagelinks table.
1005  return $content->isCountable( $hasLinks );
1006  }
1007 
1015  public function getRedirectTarget() {
1016  if ( !$this->mTitle->isRedirect() ) {
1017  return null;
1018  }
1019 
1020  if ( $this->mRedirectTarget !== null ) {
1021  return $this->mRedirectTarget;
1022  }
1023 
1024  // Query the redirect table
1025  $dbr = wfGetDB( DB_REPLICA );
1026  $row = $dbr->selectRow( 'redirect',
1027  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1028  [ 'rd_from' => $this->getId() ],
1029  __METHOD__
1030  );
1031 
1032  // rd_fragment and rd_interwiki were added later, populate them if empty
1033  if ( $row && $row->rd_fragment !== null && $row->rd_interwiki !== null ) {
1034  // (T203942) We can't redirect to Media namespace because it's virtual.
1035  // We don't want to modify Title objects farther down the
1036  // line. So, let's fix this here by changing to File namespace.
1037  if ( $row->rd_namespace == NS_MEDIA ) {
1038  $namespace = NS_FILE;
1039  } else {
1040  $namespace = $row->rd_namespace;
1041  }
1042  $this->mRedirectTarget = Title::makeTitle(
1043  $namespace, $row->rd_title,
1044  $row->rd_fragment, $row->rd_interwiki
1045  );
1046  return $this->mRedirectTarget;
1047  }
1048 
1049  // This page doesn't have an entry in the redirect table
1050  $this->mRedirectTarget = $this->insertRedirect();
1051  return $this->mRedirectTarget;
1052  }
1053 
1062  public function insertRedirect() {
1063  $content = $this->getContent();
1064  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1065  if ( !$retval ) {
1066  return null;
1067  }
1068 
1069  // Update the DB post-send if the page has not cached since now
1070  $latest = $this->getLatest();
1072  function () use ( $retval, $latest ) {
1073  $this->insertRedirectEntry( $retval, $latest );
1074  },
1076  wfGetDB( DB_MASTER )
1077  );
1078 
1079  return $retval;
1080  }
1081 
1088  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1089  $dbw = wfGetDB( DB_MASTER );
1090  $dbw->startAtomic( __METHOD__ );
1091 
1092  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1093  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1094  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1095  $dbw->upsert(
1096  'redirect',
1097  [
1098  'rd_from' => $this->getId(),
1099  'rd_namespace' => $rt->getNamespace(),
1100  'rd_title' => $rt->getDBkey(),
1101  'rd_fragment' => $truncatedFragment,
1102  'rd_interwiki' => $rt->getInterwiki(),
1103  ],
1104  'rd_from',
1105  [
1106  'rd_namespace' => $rt->getNamespace(),
1107  'rd_title' => $rt->getDBkey(),
1108  'rd_fragment' => $truncatedFragment,
1109  'rd_interwiki' => $rt->getInterwiki(),
1110  ],
1111  __METHOD__
1112  );
1113  $success = true;
1114  } else {
1115  $success = false;
1116  }
1117 
1118  $dbw->endAtomic( __METHOD__ );
1119 
1120  return $success;
1121  }
1122 
1128  public function followRedirect() {
1129  return $this->getRedirectURL( $this->getRedirectTarget() );
1130  }
1131 
1139  public function getRedirectURL( $rt ) {
1140  if ( !$rt ) {
1141  return false;
1142  }
1143 
1144  if ( $rt->isExternal() ) {
1145  if ( $rt->isLocal() ) {
1146  // Offsite wikis need an HTTP redirect.
1147  // This can be hard to reverse and may produce loops,
1148  // so they may be disabled in the site configuration.
1149  $source = $this->mTitle->getFullURL( 'redirect=no' );
1150  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1151  } else {
1152  // External pages without "local" bit set are not valid
1153  // redirect targets
1154  return false;
1155  }
1156  }
1157 
1158  if ( $rt->isSpecialPage() ) {
1159  // Gotta handle redirects to special pages differently:
1160  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1161  // Some pages are not valid targets.
1162  if ( $rt->isValidRedirectTarget() ) {
1163  return $rt->getFullURL();
1164  } else {
1165  return false;
1166  }
1167  }
1168 
1169  return $rt;
1170  }
1171 
1177  public function getContributors() {
1178  // @todo: This is expensive; cache this info somewhere.
1179 
1180  $dbr = wfGetDB( DB_REPLICA );
1181 
1182  $actorMigration = ActorMigration::newMigration();
1183  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1184 
1185  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1186 
1187  $fields = [
1188  'user_id' => $actorQuery['fields']['rev_user'],
1189  'user_name' => $actorQuery['fields']['rev_user_text'],
1190  'actor_id' => $actorQuery['fields']['rev_actor'],
1191  'user_real_name' => 'MIN(user_real_name)',
1192  'timestamp' => 'MAX(rev_timestamp)',
1193  ];
1194 
1195  $conds = [ 'rev_page' => $this->getId() ];
1196 
1197  // The user who made the top revision gets credited as "this page was last edited by
1198  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1199  $user = $this->getUser()
1200  ? User::newFromId( $this->getUser() )
1201  : User::newFromName( $this->getUserText(), false );
1202  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1203 
1204  // Username hidden?
1205  $conds[] = "{$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER )} = 0";
1206 
1207  $jconds = [
1208  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1209  ] + $actorQuery['joins'];
1210 
1211  $options = [
1212  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1213  'ORDER BY' => 'timestamp DESC',
1214  ];
1215 
1216  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1217  return new UserArrayFromResult( $res );
1218  }
1219 
1227  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1228  return $parserOptions->getStubThreshold() == 0
1229  && $this->exists()
1230  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1231  && $this->getContentHandler()->isParserCacheSupported();
1232  }
1233 
1249  public function getParserOutput(
1250  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1251  ) {
1252  $useParserCache =
1253  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1254 
1255  if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1256  throw new InvalidArgumentException(
1257  'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1258  );
1259  }
1260 
1261  wfDebug( __METHOD__ .
1262  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) );
1263  if ( $parserOptions->getStubThreshold() ) {
1264  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1265  $stats->updateCount( 'pcache.miss.stub', 1 );
1266  }
1267 
1268  if ( $useParserCache ) {
1269  $parserOutput = $this->getParserCache()
1270  ->get( $this, $parserOptions );
1271  if ( $parserOutput !== false ) {
1272  return $parserOutput;
1273  }
1274  }
1275 
1276  if ( $oldid === null || $oldid === 0 ) {
1277  $oldid = $this->getLatest();
1278  }
1279 
1280  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1281  $pool->execute();
1282 
1283  return $pool->getParserOutput();
1284  }
1285 
1291  public function doViewUpdates( User $user, $oldid = 0 ) {
1292  if ( wfReadOnly() ) {
1293  return;
1294  }
1295 
1296  // Update newtalk / watchlist notification status;
1297  // Avoid outage if the master is not reachable by using a deferred updated
1299  function () use ( $user, $oldid ) {
1300  $this->getHookRunner()->onPageViewUpdates( $this, $user );
1301 
1302  $user->clearNotification( $this->mTitle, $oldid );
1303  },
1305  );
1306  }
1307 
1314  public function doPurge() {
1315  if ( !$this->getHookRunner()->onArticlePurge( $this ) ) {
1316  return false;
1317  }
1318 
1319  $this->mTitle->invalidateCache();
1320 
1321  // Clear file cache and send purge after above page_touched update was committed
1322  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1323  $hcu->purgeTitleUrls( $this->mTitle, $hcu::PURGE_PRESEND );
1324 
1325  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1326  MediaWikiServices::getInstance()->getMessageCache()
1327  ->updateMessageOverride( $this->mTitle, $this->getContent() );
1328  }
1329 
1330  return true;
1331  }
1332 
1349  public function insertOn( $dbw, $pageId = null ) {
1350  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1351  $dbw->insert(
1352  'page',
1353  [
1354  'page_namespace' => $this->mTitle->getNamespace(),
1355  'page_title' => $this->mTitle->getDBkey(),
1356  'page_restrictions' => '',
1357  'page_is_redirect' => 0, // Will set this shortly...
1358  'page_is_new' => 1,
1359  'page_random' => wfRandom(),
1360  'page_touched' => $dbw->timestamp(),
1361  'page_latest' => 0, // Fill this in shortly...
1362  'page_len' => 0, // Fill this in shortly...
1363  ] + $pageIdForInsert,
1364  __METHOD__,
1365  [ 'IGNORE' ]
1366  );
1367 
1368  if ( $dbw->affectedRows() > 0 ) {
1369  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1370  $this->mId = $newid;
1371  $this->mTitle->resetArticleID( $newid );
1372 
1373  return $newid;
1374  } else {
1375  return false; // nothing changed
1376  }
1377  }
1378 
1394  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1395  $lastRevIsRedirect = null
1396  ) {
1397  // TODO: move into PageUpdater or PageStore
1398  // NOTE: when doing that, make sure cached fields get reset in doEditContent,
1399  // and in the compat stub!
1400 
1401  // Assertion to try to catch T92046
1402  if ( (int)$revision->getId() === 0 ) {
1403  throw new InvalidArgumentException(
1404  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1405  );
1406  }
1407 
1408  if ( $revision instanceof Revision ) {
1409  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
1410  $revision = $revision->getRevisionRecord();
1411  }
1412 
1413  $content = $revision->getContent( SlotRecord::MAIN );
1414  $len = $content ? $content->getSize() : 0;
1415  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1416 
1417  $conditions = [ 'page_id' => $this->getId() ];
1418 
1419  if ( $lastRevision !== null ) {
1420  // An extra check against threads stepping on each other
1421  $conditions['page_latest'] = $lastRevision;
1422  }
1423 
1424  $revId = $revision->getId();
1425  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1426 
1427  $model = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel();
1428 
1429  $row = [ /* SET */
1430  'page_latest' => $revId,
1431  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1432  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1433  'page_is_redirect' => $rt !== null ? 1 : 0,
1434  'page_len' => $len,
1435  'page_content_model' => $model,
1436  ];
1437 
1438  $dbw->update( 'page',
1439  $row,
1440  $conditions,
1441  __METHOD__ );
1442 
1443  $result = $dbw->affectedRows() > 0;
1444  if ( $result ) {
1445  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1446  $this->setLastEdit( $revision );
1447  $this->mLatest = $revision->getId();
1448  $this->mIsRedirect = (bool)$rt;
1449  // Update the LinkCache.
1450  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1451  $linkCache->addGoodLinkObj(
1452  $this->getId(),
1453  $this->mTitle,
1454  $len,
1455  $this->mIsRedirect,
1456  $this->mLatest,
1457  $model
1458  );
1459  }
1460 
1461  return $result;
1462  }
1463 
1475  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1476  // Always update redirects (target link might have changed)
1477  // Update/Insert if we don't know if the last revision was a redirect or not
1478  // Delete if changing from redirect to non-redirect
1479  $isRedirect = $redirectTitle !== null;
1480 
1481  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1482  return true;
1483  }
1484 
1485  if ( $isRedirect ) {
1486  $success = $this->insertRedirectEntry( $redirectTitle );
1487  } else {
1488  // This is not a redirect, remove row from redirect table
1489  $where = [ 'rd_from' => $this->getId() ];
1490  $dbw->delete( 'redirect', $where, __METHOD__ );
1491  $success = true;
1492  }
1493 
1494  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1495  MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
1496  ->invalidateImageRedirect( $this->getTitle() );
1497  }
1498 
1499  return $success;
1500  }
1501 
1512  public function updateIfNewerOn( $dbw, $revision ) {
1513  wfDeprecated( __METHOD__, '1.24' );
1514 
1515  $revisionRecord = $revision->getRevisionRecord();
1516 
1517  $row = $dbw->selectRow(
1518  [ 'revision', 'page' ],
1519  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1520  [
1521  'page_id' => $this->getId(),
1522  'page_latest=rev_id'
1523  ],
1524  __METHOD__
1525  );
1526 
1527  if ( $row ) {
1528  $rowTimestamp = MWTimestamp::convert( TS_MW, $row->rev_timestamp );
1529  if ( $rowTimestamp >= $revisionRecord->getTimestamp() ) {
1530  return false;
1531  }
1532  $prev = $row->rev_id;
1533  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1534  } else {
1535  // No or missing previous revision; mark the page as new
1536  $prev = 0;
1537  $lastRevIsRedirect = null;
1538  }
1539 
1540  $ret = $this->updateRevisionOn(
1541  $dbw,
1542  $revisionRecord,
1543  $prev,
1544  $lastRevIsRedirect
1545  );
1546 
1547  return $ret;
1548  }
1549 
1562  public static function hasDifferencesOutsideMainSlot( $a, $b ) {
1563  if ( $a instanceof Revision ) {
1564  wfDeprecated( __METHOD__ . ' with Revision objects', '1.35' );
1565  $a = $a->getRevisionRecord();
1566  }
1567  if ( $b instanceof Revision ) {
1568  wfDeprecated( __METHOD__ . ' with Revision objects', '1.35' );
1569  $b = $b->getRevisionRecord();
1570  }
1571  $aSlots = $a->getSlots();
1572  $bSlots = $b->getSlots();
1573  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1574 
1575  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1576  }
1577 
1591  public function getUndoContent( Revision $undo, Revision $undoafter ) {
1592  wfDeprecated( __METHOD__, '1.35' );
1593  // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1594 
1595  if ( self::hasDifferencesOutsideMainSlot(
1596  $undo->getRevisionRecord(),
1597  $undoafter->getRevisionRecord()
1598  ) ) {
1599  // Cannot yet undo edits that involve anything other the main slot.
1600  return false;
1601  }
1602 
1603  $handler = $undo->getContentHandler();
1604 
1605  // TODO remove use of Revision objects by deprecating this method entirely
1606  $revRecord = $this->getRevisionRecord();
1607  $revision = $revRecord ? new Revision( $revRecord ) : null;
1608  return $handler->getUndoContent( $revision, $undo, $undoafter );
1609  }
1610 
1621  public function supportsSections() {
1622  return $this->getContentHandler()->supportsSections();
1623  }
1624 
1639  public function replaceSectionContent(
1640  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1641  ) {
1642  $baseRevId = null;
1643  if ( $edittime && $sectionId !== 'new' ) {
1644  $lb = $this->getDBLoadBalancer();
1645  $rev = $this->getRevisionStore()->getRevisionByTimestamp( $this->mTitle, $edittime );
1646  // Try the master if this thread may have just added it.
1647  // This could be abstracted into a Revision method, but we don't want
1648  // to encourage loading of revisions by timestamp.
1649  if ( !$rev
1650  && $lb->getServerCount() > 1
1651  && $lb->hasOrMadeRecentMasterChanges()
1652  ) {
1653  $rev = $this->getRevisionStore()->getRevisionByTimestamp(
1654  $this->mTitle, $edittime, RevisionStore::READ_LATEST );
1655  }
1656  if ( $rev ) {
1657  $baseRevId = $rev->getId();
1658  }
1659  }
1660 
1661  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1662  }
1663 
1677  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1678  $sectionTitle = '', $baseRevId = null
1679  ) {
1680  if ( strval( $sectionId ) === '' ) {
1681  // Whole-page edit; let the whole text through
1682  $newContent = $sectionContent;
1683  } else {
1684  if ( !$this->supportsSections() ) {
1685  throw new MWException( "sections not supported for content model " .
1686  $this->getContentHandler()->getModelID() );
1687  }
1688 
1689  // T32711: always use current version when adding a new section
1690  if ( $baseRevId === null || $sectionId === 'new' ) {
1691  $oldContent = $this->getContent();
1692  } else {
1693  $revRecord = $this->getRevisionStore()->getRevisionById( $baseRevId );
1694  if ( !$revRecord ) {
1695  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1696  $this->getId() . "; section: $sectionId)" );
1697  return null;
1698  }
1699 
1700  $oldContent = $revRecord->getContent( SlotRecord::MAIN );
1701  }
1702 
1703  if ( !$oldContent ) {
1704  wfDebug( __METHOD__ . ": no page text" );
1705  return null;
1706  }
1707 
1708  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1709  }
1710 
1711  return $newContent;
1712  }
1713 
1723  public function checkFlags( $flags ) {
1724  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1725  if ( $this->exists() ) {
1726  $flags |= EDIT_UPDATE;
1727  } else {
1728  $flags |= EDIT_NEW;
1729  }
1730  }
1731 
1732  return $flags;
1733  }
1734 
1738  private function newDerivedDataUpdater() {
1740 
1741  $services = MediaWikiServices::getInstance();
1743  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1744  $this->getRevisionStore(),
1745  $this->getRevisionRenderer(),
1746  $this->getSlotRoleRegistry(),
1747  $this->getParserCache(),
1749  $services->getMessageCache(),
1750  $services->getContentLanguage(),
1751  $services->getDBLoadBalancerFactory(),
1752  $this->getContentHandlerFactory(),
1753  $this->getHookContainer()
1754  );
1755 
1756  $derivedDataUpdater->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
1759 
1760  return $derivedDataUpdater;
1761  }
1762 
1790  private function getDerivedDataUpdater(
1791  User $forUser = null,
1792  RevisionRecord $forRevision = null,
1793  RevisionSlotsUpdate $forUpdate = null,
1794  $forEdit = false
1795  ) {
1796  if ( !$forRevision && !$forUpdate ) {
1797  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1798  // going to use it with.
1799  $this->derivedDataUpdater = null;
1800  }
1801 
1802  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1803  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1804  // to it did not yet initialize it, because we don't know what data it will be
1805  // initialized with.
1806  $this->derivedDataUpdater = null;
1807  }
1808 
1809  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1810  // However, there is no good way to construct a cache key. We'd need to check against all
1811  // cached instances.
1812 
1813  if ( $this->derivedDataUpdater
1814  && !$this->derivedDataUpdater->isReusableFor(
1815  $forUser,
1816  $forRevision,
1817  $forUpdate,
1818  $forEdit ? $this->getLatest() : null
1819  )
1820  ) {
1821  $this->derivedDataUpdater = null;
1822  }
1823 
1824  if ( !$this->derivedDataUpdater ) {
1825  $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1826  }
1827 
1829  }
1830 
1846  public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1848 
1849  $pageUpdater = new PageUpdater(
1850  $user,
1851  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1852  $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1853  $this->getDBLoadBalancer(),
1854  $this->getRevisionStore(),
1855  $this->getSlotRoleRegistry(),
1856  $this->getContentHandlerFactory(),
1857  $this->getHookContainer()
1858  );
1859 
1860  $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
1861  $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
1862  $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
1863 
1864  return $pageUpdater;
1865  }
1866 
1931  public function doEditContent(
1932  Content $content, $summary, $flags = 0, $originalRevId = false,
1933  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1934  ) {
1935  global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1936 
1937  if ( !( $summary instanceof CommentStoreComment ) ) {
1938  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1939  }
1940 
1941  if ( !$user ) {
1942  $user = $wgUser;
1943  }
1944 
1945  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1946  // Checking the minoredit right should be done in the same place the 'bot' right is
1947  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1948  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1949  if ( ( $flags & EDIT_MINOR ) && !$permissionManager->userHasRight( $user, 'minoredit' ) ) {
1950  $flags = ( $flags & ~EDIT_MINOR );
1951  }
1952 
1953  $slotsUpdate = new RevisionSlotsUpdate();
1954  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1955 
1956  // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1957  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1958  // used by this PageUpdater. However, there is no guarantee for this.
1959  $updater = $this->newPageUpdater( $user, $slotsUpdate );
1960  $updater->setContent( SlotRecord::MAIN, $content );
1961  $updater->setOriginalRevisionId( $originalRevId );
1962 
1963  if ( $undidRevId !== 0 ) {
1964  $updater->markAsRevert( EditResult::REVERT_UNDO, $undidRevId );
1965  }
1966 
1967  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1968 
1969  // TODO: this logic should not be in the storage layer, it's here for compatibility
1970  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1971  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1972 
1973  if ( $needsPatrol && $permissionManager->userCan(
1974  'autopatrol', $user, $this->getTitle()
1975  ) ) {
1976  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1977  }
1978 
1979  $updater->addTags( $tags );
1980 
1981  $revRec = $updater->saveRevision(
1982  $summary,
1983  $flags
1984  );
1985 
1986  // $revRec will be null if the edit failed, or if no new revision was created because
1987  // the content did not change.
1988  if ( $revRec ) {
1989  // update cached fields
1990  // TODO: this is currently redundant to what is done in updateRevisionOn.
1991  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1992  $this->setLastEdit( $revRec );
1993  $this->mLatest = $revRec->getId();
1994  }
1995 
1996  return $updater->getStatus();
1997  }
1998 
2013  public function makeParserOptions( $context ) {
2014  $options = ParserOptions::newCanonical( $context );
2015 
2016  if ( $this->getTitle()->isConversionTable() ) {
2017  // @todo ConversionTable should become a separate content model, so
2018  // we don't need special cases like this one.
2019  $options->disableContentConversion();
2020  }
2021 
2022  return $options;
2023  }
2024 
2044  public function prepareContentForEdit(
2045  Content $content,
2046  $revision = null,
2047  User $user = null,
2048  $serialFormat = null,
2049  $useCache = true
2050  ) {
2051  global $wgUser;
2052 
2053  if ( !$user ) {
2054  $user = $wgUser;
2055  }
2056 
2057  if ( $revision !== null ) {
2058  if ( $revision instanceof Revision ) {
2059  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
2060  $revision = $revision->getRevisionRecord();
2061  } elseif ( !( $revision instanceof RevisionRecord ) ) {
2062  throw new InvalidArgumentException(
2063  __METHOD__ . ': invalid $revision argument type ' . gettype( $revision ) );
2064  }
2065  }
2066 
2067  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2068  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2069 
2070  if ( !$updater->isUpdatePrepared() ) {
2071  $updater->prepareContent( $user, $slots, $useCache );
2072 
2073  if ( $revision ) {
2074  $updater->prepareUpdate(
2075  $revision,
2076  [
2077  'causeAction' => 'prepare-edit',
2078  'causeAgent' => $user->getName(),
2079  ]
2080  );
2081  }
2082  }
2083 
2084  return $updater->getPreparedEdit();
2085  }
2086 
2116  public function doEditUpdates( $revisionRecord, User $user, array $options = [] ) {
2117  if ( $revisionRecord instanceof Revision ) {
2118  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
2119  $revisionRecord = $revisionRecord->getRevisionRecord();
2120  }
2121  if ( isset( $options['oldrevision'] ) && $options['oldrevision'] instanceof Revision ) {
2122  wfDeprecated(
2123  __METHOD__ . ' with the `oldrevision` option being a ' .
2124  'Revision object',
2125  '1.35'
2126  );
2127  $options['oldrevision'] = $options['oldrevision']->getRevisionRecord();
2128  }
2129 
2130  $options += [
2131  'causeAction' => 'edit-page',
2132  'causeAgent' => $user->getName(),
2133  ];
2134 
2135  $updater = $this->getDerivedDataUpdater( $user, $revisionRecord );
2136 
2137  $updater->prepareUpdate( $revisionRecord, $options );
2138 
2139  $updater->doUpdates();
2140  }
2141 
2155  public function updateParserCache( array $options = [] ) {
2156  $revision = $this->getRevisionRecord();
2157  if ( !$revision || !$revision->getId() ) {
2158  LoggerFactory::getInstance( 'wikipage' )->info(
2159  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2160  );
2161  return;
2162  }
2163  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2164 
2165  $updater = $this->getDerivedDataUpdater( $user, $revision );
2166  $updater->prepareUpdate( $revision, $options );
2167  $updater->doParserCacheUpdate();
2168  }
2169 
2199  public function doSecondaryDataUpdates( array $options = [] ) {
2200  $options['recursive'] = $options['recursive'] ?? true;
2201  $revision = $this->getRevisionRecord();
2202  if ( !$revision || !$revision->getId() ) {
2203  LoggerFactory::getInstance( 'wikipage' )->info(
2204  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2205  );
2206  return;
2207  }
2208  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2209 
2210  $updater = $this->getDerivedDataUpdater( $user, $revision );
2211  $updater->prepareUpdate( $revision, $options );
2212  $updater->doSecondaryDataUpdates( $options );
2213  }
2214 
2229  public function doUpdateRestrictions( array $limit, array $expiry,
2230  &$cascade, $reason, User $user, $tags = null
2231  ) {
2233 
2234  if ( wfReadOnly() ) {
2235  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2236  }
2237 
2238  $this->loadPageData( 'fromdbmaster' );
2239  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2240  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2241  $id = $this->getId();
2242 
2243  if ( !$cascade ) {
2244  $cascade = false;
2245  }
2246 
2247  // Take this opportunity to purge out expired restrictions
2249 
2250  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2251  // we expect a single selection, but the schema allows otherwise.
2252  $isProtected = false;
2253  $protect = false;
2254  $changed = false;
2255 
2256  $dbw = wfGetDB( DB_MASTER );
2257 
2258  foreach ( $restrictionTypes as $action ) {
2259  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2260  $expiry[$action] = 'infinity';
2261  }
2262  if ( !isset( $limit[$action] ) ) {
2263  $limit[$action] = '';
2264  } elseif ( $limit[$action] != '' ) {
2265  $protect = true;
2266  }
2267 
2268  // Get current restrictions on $action
2269  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2270  if ( $current != '' ) {
2271  $isProtected = true;
2272  }
2273 
2274  if ( $limit[$action] != $current ) {
2275  $changed = true;
2276  } elseif ( $limit[$action] != '' ) {
2277  // Only check expiry change if the action is actually being
2278  // protected, since expiry does nothing on an not-protected
2279  // action.
2280  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2281  $changed = true;
2282  }
2283  }
2284  }
2285 
2286  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2287  $changed = true;
2288  }
2289 
2290  // If nothing has changed, do nothing
2291  if ( !$changed ) {
2292  return Status::newGood();
2293  }
2294 
2295  if ( !$protect ) { // No protection at all means unprotection
2296  $revCommentMsg = 'unprotectedarticle-comment';
2297  $logAction = 'unprotect';
2298  } elseif ( $isProtected ) {
2299  $revCommentMsg = 'modifiedarticleprotection-comment';
2300  $logAction = 'modify';
2301  } else {
2302  $revCommentMsg = 'protectedarticle-comment';
2303  $logAction = 'protect';
2304  }
2305 
2306  $logRelationsValues = [];
2307  $logRelationsField = null;
2308  $logParamsDetails = [];
2309 
2310  // Null revision (used for change tag insertion)
2311  $nullRevision = null;
2312 
2313  if ( $id ) { // Protection of existing page
2314  if ( !$this->getHookRunner()->onArticleProtect( $this, $user, $limit, $reason ) ) {
2315  return Status::newGood();
2316  }
2317 
2318  // Only certain restrictions can cascade...
2319  $editrestriction = isset( $limit['edit'] )
2320  ? [ $limit['edit'] ]
2321  : $this->mTitle->getRestrictions( 'edit' );
2322  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2323  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2324  }
2325  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2326  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2327  }
2328 
2329  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2330  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2331  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2332  }
2333  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2334  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2335  }
2336 
2337  // The schema allows multiple restrictions
2338  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2339  $cascade = false;
2340  }
2341 
2342  // insert null revision to identify the page protection change as edit summary
2343  $latest = $this->getLatest();
2344  $nullRevisionRecord = $this->insertNullProtectionRevision(
2345  $revCommentMsg,
2346  $limit,
2347  $expiry,
2348  $cascade,
2349  $reason,
2350  $user
2351  );
2352 
2353  if ( $nullRevisionRecord === null ) {
2354  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2355  }
2356 
2357  $logRelationsField = 'pr_id';
2358 
2359  // T214035: Avoid deadlock on MySQL.
2360  // Do a DELETE by primary key (pr_id) for any existing protection rows.
2361  // On MySQL and derivatives, unconditionally deleting by page ID (pr_page) would.
2362  // place a gap lock if there are no matching rows. This can deadlock when another
2363  // thread modifies protection settings for page IDs in the same gap.
2364  $existingProtectionIds = $dbw->selectFieldValues(
2365  'page_restrictions',
2366  'pr_id',
2367  [
2368  'pr_page' => $id,
2369  'pr_type' => array_map( 'strval', array_keys( $limit ) )
2370  ],
2371  __METHOD__
2372  );
2373 
2374  if ( $existingProtectionIds ) {
2375  $dbw->delete(
2376  'page_restrictions',
2377  [ 'pr_id' => $existingProtectionIds ],
2378  __METHOD__
2379  );
2380  }
2381 
2382  // Update restrictions table
2383  foreach ( $limit as $action => $restrictions ) {
2384  if ( $restrictions != '' ) {
2385  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2386  $dbw->insert(
2387  'page_restrictions',
2388  [
2389  'pr_page' => $id,
2390  'pr_type' => $action,
2391  'pr_level' => $restrictions,
2392  'pr_cascade' => $cascadeValue,
2393  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2394  ],
2395  __METHOD__
2396  );
2397  $logRelationsValues[] = $dbw->insertId();
2398  $logParamsDetails[] = [
2399  'type' => $action,
2400  'level' => $restrictions,
2401  'expiry' => $expiry[$action],
2402  'cascade' => (bool)$cascadeValue,
2403  ];
2404  }
2405  }
2406 
2407  // Clear out legacy restriction fields
2408  $dbw->update(
2409  'page',
2410  [ 'page_restrictions' => '' ],
2411  [ 'page_id' => $id ],
2412  __METHOD__
2413  );
2414 
2415  $this->getHookRunner()->onRevisionFromEditComplete(
2416  $this, $nullRevisionRecord, $latest, $user, $tags );
2417 
2418  // Hook is hard deprecated since 1.35
2419  if ( $this->getHookContainer()->isRegistered( 'NewRevisionFromEditComplete' ) ) {
2420  // Only create the Revision object if neeed
2421  $nullRevision = new Revision( $nullRevisionRecord );
2422  $this->getHookRunner()->onNewRevisionFromEditComplete(
2423  $this, $nullRevision, $latest, $user, $tags );
2424  }
2425 
2426  $this->getHookRunner()->onArticleProtectComplete( $this, $user, $limit, $reason );
2427  } else { // Protection of non-existing page (also known as "title protection")
2428  // Cascade protection is meaningless in this case
2429  $cascade = false;
2430 
2431  if ( $limit['create'] != '' ) {
2432  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2433  $dbw->replace( 'protected_titles',
2434  [ [ 'pt_namespace', 'pt_title' ] ],
2435  [
2436  'pt_namespace' => $this->mTitle->getNamespace(),
2437  'pt_title' => $this->mTitle->getDBkey(),
2438  'pt_create_perm' => $limit['create'],
2439  'pt_timestamp' => $dbw->timestamp(),
2440  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2441  'pt_user' => $user->getId(),
2442  ] + $commentFields, __METHOD__
2443  );
2444  $logParamsDetails[] = [
2445  'type' => 'create',
2446  'level' => $limit['create'],
2447  'expiry' => $expiry['create'],
2448  ];
2449  } else {
2450  $dbw->delete( 'protected_titles',
2451  [
2452  'pt_namespace' => $this->mTitle->getNamespace(),
2453  'pt_title' => $this->mTitle->getDBkey()
2454  ], __METHOD__
2455  );
2456  }
2457  }
2458 
2459  $this->mTitle->flushRestrictions();
2460  InfoAction::invalidateCache( $this->mTitle );
2461 
2462  if ( $logAction == 'unprotect' ) {
2463  $params = [];
2464  } else {
2465  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2466  $params = [
2467  '4::description' => $protectDescriptionLog, // parameter for IRC
2468  '5:bool:cascade' => $cascade,
2469  'details' => $logParamsDetails, // parameter for localize and api
2470  ];
2471  }
2472 
2473  // Update the protection log
2474  $logEntry = new ManualLogEntry( 'protect', $logAction );
2475  $logEntry->setTarget( $this->mTitle );
2476  $logEntry->setComment( $reason );
2477  $logEntry->setPerformer( $user );
2478  $logEntry->setParameters( $params );
2479  if ( $nullRevision !== null ) {
2480  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2481  }
2482  $logEntry->addTags( $tags );
2483  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2484  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2485  }
2486  $logId = $logEntry->insert();
2487  $logEntry->publish( $logId );
2488 
2489  return Status::newGood( $logId );
2490  }
2491 
2505  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2506  array $expiry, $cascade, $reason, $user = null
2507  ) {
2508  wfDeprecated( __METHOD__, '1.35' );
2509  if ( !$user ) {
2510  global $wgUser;
2511  $user = $wgUser;
2512  }
2513 
2514  $nullRevRecord = $this->insertNullProtectionRevision(
2515  $revCommentMsg,
2516  $limit,
2517  $expiry,
2518  (bool)$cascade,
2519  $reason,
2520  $user
2521  );
2522  return $nullRevRecord ? new Revision( $nullRevRecord ) : null;
2523  }
2524 
2537  string $revCommentMsg,
2538  array $limit,
2539  array $expiry,
2540  bool $cascade,
2541  string $reason,
2542  User $user
2543  ) : ?RevisionRecord {
2544  $dbw = wfGetDB( DB_MASTER );
2545 
2546  // Prepare a null revision to be added to the history
2547  $editComment = wfMessage(
2548  $revCommentMsg,
2549  $this->mTitle->getPrefixedText(),
2550  $user->getName()
2551  )->inContentLanguage()->text();
2552  if ( $reason ) {
2553  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2554  }
2555  $protectDescription = $this->protectDescription( $limit, $expiry );
2556  if ( $protectDescription ) {
2557  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2558  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2559  ->inContentLanguage()->text();
2560  }
2561  if ( $cascade ) {
2562  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2563  $editComment .= wfMessage( 'brackets' )->params(
2564  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2565  )->inContentLanguage()->text();
2566  }
2567 
2568  $revStore = $this->getRevisionStore();
2569  $comment = CommentStoreComment::newUnsavedComment( $editComment );
2570  $nullRevRecord = $revStore->newNullRevision(
2571  $dbw,
2572  $this->getTitle(),
2573  $comment,
2574  true,
2575  $user
2576  );
2577 
2578  if ( $nullRevRecord ) {
2579  $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
2580 
2581  // Update page record and touch page
2582  $oldLatest = $inserted->getParentId();
2583 
2584  $this->updateRevisionOn( $dbw, $inserted, $oldLatest );
2585 
2586  return $inserted;
2587  } else {
2588  return null;
2589  }
2590  }
2591 
2596  protected function formatExpiry( $expiry ) {
2597  if ( $expiry != 'infinity' ) {
2598  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2599  return wfMessage(
2600  'protect-expiring',
2601  $contLang->timeanddate( $expiry, false, false ),
2602  $contLang->date( $expiry, false, false ),
2603  $contLang->time( $expiry, false, false )
2604  )->inContentLanguage()->text();
2605  } else {
2606  return wfMessage( 'protect-expiry-indefinite' )
2607  ->inContentLanguage()->text();
2608  }
2609  }
2610 
2618  public function protectDescription( array $limit, array $expiry ) {
2619  $protectDescription = '';
2620 
2621  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2622  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2623  # All possible message keys are listed here for easier grepping:
2624  # * restriction-create
2625  # * restriction-edit
2626  # * restriction-move
2627  # * restriction-upload
2628  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2629  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2630  # with '' filtered out. All possible message keys are listed below:
2631  # * protect-level-autoconfirmed
2632  # * protect-level-sysop
2633  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2634  ->inContentLanguage()->text();
2635 
2636  $expiryText = $this->formatExpiry( $expiry[$action] );
2637 
2638  if ( $protectDescription !== '' ) {
2639  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2640  }
2641  $protectDescription .= wfMessage( 'protect-summary-desc' )
2642  ->params( $actionText, $restrictionsText, $expiryText )
2643  ->inContentLanguage()->text();
2644  }
2645 
2646  return $protectDescription;
2647  }
2648 
2660  public function protectDescriptionLog( array $limit, array $expiry ) {
2661  $protectDescriptionLog = '';
2662 
2663  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2664  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2665  $expiryText = $this->formatExpiry( $expiry[$action] );
2666  $protectDescriptionLog .=
2667  $dirMark .
2668  "[$action=$restrictions] ($expiryText)";
2669  }
2670 
2671  return trim( $protectDescriptionLog );
2672  }
2673 
2686  public function isBatchedDelete( $safetyMargin = 0 ) {
2688 
2689  $dbr = wfGetDB( DB_REPLICA );
2690  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2691  $revCount += $safetyMargin;
2692 
2693  return $revCount >= $wgDeleteRevisionsBatchSize;
2694  }
2695 
2717  public function doDeleteArticle(
2718  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2719  $immediate = false
2720  ) {
2721  wfDeprecated( __METHOD__, '1.35' );
2722  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2723  [], 'delete', $immediate );
2724 
2725  // Returns true if the page was actually deleted, or is scheduled for deletion
2726  return $status->isOK();
2727  }
2728 
2753  public function doDeleteArticleReal(
2754  $reason, $user = false, $suppress = false, $u2 = null, &$error = '', User $deleter = null,
2755  $tags = [], $logsubtype = 'delete', $immediate = false
2756  ) {
2757  wfDebug( __METHOD__ );
2758 
2759  if ( $user instanceof User ) {
2760  $deleter = $user;
2761  } else {
2762  wfDeprecated(
2763  __METHOD__ . ' without passing a User as the second parameter',
2764  '1.35'
2765  );
2766  $suppress = $user;
2767  if ( $deleter === null ) {
2768  global $wgUser;
2769  $deleter = $wgUser;
2770  }
2771  }
2772  unset( $user );
2773 
2774  $status = Status::newGood();
2775 
2776  if ( !$this->getHookRunner()->onArticleDelete(
2777  $this, $deleter, $reason, $error, $status, $suppress )
2778  ) {
2779  if ( $status->isOK() ) {
2780  // Hook aborted but didn't set a fatal status
2781  $status->fatal( 'delete-hook-aborted' );
2782  }
2783  return $status;
2784  }
2785 
2786  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2787  $logsubtype, $immediate );
2788  }
2789 
2806  public function doDeleteArticleBatched(
2807  $reason, $suppress, User $deleter, $tags,
2808  $logsubtype, $immediate = false, $webRequestId = null
2809  ) {
2810  wfDebug( __METHOD__ );
2811 
2812  $status = Status::newGood();
2813 
2814  $dbw = wfGetDB( DB_MASTER );
2815  $dbw->startAtomic( __METHOD__ );
2816 
2817  $this->loadPageData( self::READ_LATEST );
2818  $id = $this->getId();
2819  // T98706: lock the page from various other updates but avoid using
2820  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2821  // the revisions queries (which also JOIN on user). Only lock the page
2822  // row and CAS check on page_latest to see if the trx snapshot matches.
2823  $lockedLatest = $this->lockAndGetLatest();
2824  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2825  $dbw->endAtomic( __METHOD__ );
2826  // Page not there or trx snapshot is stale
2827  $status->error( 'cannotdelete',
2828  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2829  return $status;
2830  }
2831 
2832  // At this point we are now committed to returning an OK
2833  // status unless some DB query error or other exception comes up.
2834  // This way callers don't have to call rollback() if $status is bad
2835  // unless they actually try to catch exceptions (which is rare).
2836 
2837  // we need to remember the old content so we can use it to generate all deletion updates.
2838  $revisionRecord = $this->getRevisionRecord();
2839  try {
2840  $content = $this->getContent( RevisionRecord::RAW );
2841  } catch ( Exception $ex ) {
2842  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2843  . $ex->getMessage() );
2844 
2845  $content = null;
2846  }
2847 
2848  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2849  // one batch of revisions and defer archival of any others to the job queue.
2850  $explictTrxLogged = false;
2851  while ( true ) {
2852  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2853  if ( $done || !$immediate ) {
2854  break;
2855  }
2856  $dbw->endAtomic( __METHOD__ );
2857  if ( $dbw->explicitTrxActive() ) {
2858  // Explict transactions may never happen here in practice. Log to be sure.
2859  if ( !$explictTrxLogged ) {
2860  $explictTrxLogged = true;
2861  LoggerFactory::getInstance( 'wfDebug' )->debug(
2862  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2863  'title' => $this->getTitle()->getText(),
2864  ] );
2865  }
2866  continue;
2867  }
2868  if ( $dbw->trxLevel() ) {
2869  $dbw->commit( __METHOD__ );
2870  }
2871  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2872  $lbFactory->waitForReplication();
2873  $dbw->startAtomic( __METHOD__ );
2874  }
2875 
2876  // If done archiving, also delete the article.
2877  if ( !$done ) {
2878  $dbw->endAtomic( __METHOD__ );
2879 
2880  $jobParams = [
2881  'namespace' => $this->getTitle()->getNamespace(),
2882  'title' => $this->getTitle()->getDBkey(),
2883  'wikiPageId' => $id,
2884  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2885  'reason' => $reason,
2886  'suppress' => $suppress,
2887  'userId' => $deleter->getId(),
2888  'tags' => json_encode( $tags ),
2889  'logsubtype' => $logsubtype,
2890  ];
2891 
2892  $job = new DeletePageJob( $jobParams );
2893  JobQueueGroup::singleton()->push( $job );
2894 
2895  $status->warning( 'delete-scheduled',
2896  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2897  } else {
2898  // Get archivedRevisionCount by db query, because there's no better alternative.
2899  // Jobs cannot pass a count of archived revisions to the next job, because additional
2900  // deletion operations can be started while the first is running. Jobs from each
2901  // gracefully interleave, but would not know about each other's count. Deduplication
2902  // in the job queue to avoid simultaneous deletion operations would add overhead.
2903  // Number of archived revisions cannot be known beforehand, because edits can be made
2904  // while deletion operations are being processed, changing the number of archivals.
2905  $archivedRevisionCount = (int)$dbw->selectField(
2906  'archive', 'COUNT(*)',
2907  [
2908  'ar_namespace' => $this->getTitle()->getNamespace(),
2909  'ar_title' => $this->getTitle()->getDBkey(),
2910  'ar_page_id' => $id
2911  ], __METHOD__
2912  );
2913 
2914  // Clone the title and wikiPage, so we have the information we need when
2915  // we log and run the ArticleDeleteComplete hook.
2916  $logTitle = clone $this->mTitle;
2917  $wikiPageBeforeDelete = clone $this;
2918 
2919  // Now that it's safely backed up, delete it
2920  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2921 
2922  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2923  $logtype = $suppress ? 'suppress' : 'delete';
2924 
2925  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2926  $logEntry->setPerformer( $deleter );
2927  $logEntry->setTarget( $logTitle );
2928  $logEntry->setComment( $reason );
2929  $logEntry->addTags( $tags );
2930  $logid = $logEntry->insert();
2931 
2932  $dbw->onTransactionPreCommitOrIdle(
2933  function () use ( $logEntry, $logid ) {
2934  // T58776: avoid deadlocks (especially from FileDeleteForm)
2935  $logEntry->publish( $logid );
2936  },
2937  __METHOD__
2938  );
2939 
2940  $dbw->endAtomic( __METHOD__ );
2941 
2942  $this->doDeleteUpdates(
2943  $id,
2944  $content,
2945  $revisionRecord,
2946  $deleter
2947  );
2948 
2949  $this->getHookRunner()->onArticleDeleteComplete(
2950  $wikiPageBeforeDelete,
2951  $deleter,
2952  $reason,
2953  $id,
2954  $content,
2955  $logEntry,
2956  $archivedRevisionCount
2957  );
2958  $status->value = $logid;
2959 
2960  // Show log excerpt on 404 pages rather than just a link
2961  $dbCache = ObjectCache::getInstance( 'db-replicated' );
2962  $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2963  $dbCache->set( $key, 1, $dbCache::TTL_DAY );
2964  }
2965 
2966  return $status;
2967  }
2968 
2978  protected function archiveRevisions( $dbw, $id, $suppress ) {
2980 
2981  // Given the lock above, we can be confident in the title and page ID values
2982  $namespace = $this->getTitle()->getNamespace();
2983  $dbKey = $this->getTitle()->getDBkey();
2984 
2985  $commentStore = CommentStore::getStore();
2986  $actorMigration = ActorMigration::newMigration();
2987 
2988  $revQuery = $this->getRevisionStore()->getQueryInfo();
2989  $bitfield = false;
2990 
2991  // Bitfields to further suppress the content
2992  if ( $suppress ) {
2993  $bitfield = RevisionRecord::SUPPRESSED_ALL;
2994  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2995  }
2996 
2997  // For now, shunt the revision data into the archive table.
2998  // Text is *not* removed from the text table; bulk storage
2999  // is left intact to avoid breaking block-compression or
3000  // immutable storage schemes.
3001  // In the future, we may keep revisions and mark them with
3002  // the rev_deleted field, which is reserved for this purpose.
3003 
3004  // Lock rows in `revision` and its temp tables, but not any others.
3005  // Note array_intersect() preserves keys from the first arg, and we're
3006  // assuming $revQuery has `revision` primary and isn't using subtables
3007  // for anything we care about.
3008  $dbw->lockForUpdate(
3009  array_intersect(
3010  $revQuery['tables'],
3011  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
3012  ),
3013  [ 'rev_page' => $id ],
3014  __METHOD__,
3015  [],
3016  $revQuery['joins']
3017  );
3018 
3019  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
3020  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
3021  $res = $dbw->select(
3022  $revQuery['tables'],
3023  $revQuery['fields'],
3024  [ 'rev_page' => $id ],
3025  __METHOD__,
3026  [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
3027  $revQuery['joins']
3028  );
3029 
3030  // Build their equivalent archive rows
3031  $rowsInsert = [];
3032  $revids = [];
3033 
3035  $ipRevIds = [];
3036 
3037  $done = true;
3038  foreach ( $res as $row ) {
3039  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
3040  $done = false;
3041  break;
3042  }
3043 
3044  $comment = $commentStore->getComment( 'rev_comment', $row );
3045  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
3046  $rowInsert = [
3047  'ar_namespace' => $namespace,
3048  'ar_title' => $dbKey,
3049  'ar_timestamp' => $row->rev_timestamp,
3050  'ar_minor_edit' => $row->rev_minor_edit,
3051  'ar_rev_id' => $row->rev_id,
3052  'ar_parent_id' => $row->rev_parent_id,
3053  'ar_len' => $row->rev_len,
3054  'ar_page_id' => $id,
3055  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
3056  'ar_sha1' => $row->rev_sha1,
3057  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
3058  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
3059 
3060  $rowsInsert[] = $rowInsert;
3061  $revids[] = $row->rev_id;
3062 
3063  // Keep track of IP edits, so that the corresponding rows can
3064  // be deleted in the ip_changes table.
3065  if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
3066  $ipRevIds[] = $row->rev_id;
3067  }
3068  }
3069 
3070  // This conditional is just a sanity check
3071  if ( count( $revids ) > 0 ) {
3072  // Copy them into the archive table
3073  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
3074 
3075  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
3076  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
3077  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
3078 
3079  // Also delete records from ip_changes as applicable.
3080  if ( count( $ipRevIds ) > 0 ) {
3081  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
3082  }
3083  }
3084 
3085  return $done;
3086  }
3087 
3094  public function lockAndGetLatest() {
3095  return (int)wfGetDB( DB_MASTER )->selectField(
3096  'page',
3097  'page_latest',
3098  [
3099  'page_id' => $this->getId(),
3100  // Typically page_id is enough, but some code might try to do
3101  // updates assuming the title is the same, so verify that
3102  'page_namespace' => $this->getTitle()->getNamespace(),
3103  'page_title' => $this->getTitle()->getDBkey()
3104  ],
3105  __METHOD__,
3106  [ 'FOR UPDATE' ]
3107  );
3108  }
3109 
3123  public function doDeleteUpdates(
3124  $id, Content $content = null, $revRecord = null, User $user = null
3125  ) {
3126  if ( $revRecord && $revRecord instanceof Revision ) {
3127  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
3128  $revRecord = $revRecord->getRevisionRecord();
3129  }
3130 
3131  if ( $id !== $this->getId() ) {
3132  throw new InvalidArgumentException( 'Mismatching page ID' );
3133  }
3134 
3135  try {
3136  $countable = $this->isCountable();
3137  } catch ( Exception $ex ) {
3138  // fallback for deleting broken pages for which we cannot load the content for
3139  // some reason. Note that doDeleteArticleReal() already logged this problem.
3140  $countable = false;
3141  }
3142 
3143  // Update site status
3145  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3146  ) );
3147 
3148  // Delete pagelinks, update secondary indexes, etc
3149  $updates = $this->getDeletionUpdates( $revRecord ?: $content );
3150  foreach ( $updates as $update ) {
3151  DeferredUpdates::addUpdate( $update );
3152  }
3153 
3154  $causeAgent = $user ? $user->getName() : 'unknown';
3155  // Reparse any pages transcluding this page
3157  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3158  // Reparse any pages including this image
3159  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3161  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3162  }
3163 
3164  // Clear caches
3165  self::onArticleDelete( $this->mTitle );
3166 
3168  $this->mTitle,
3169  $revRecord,
3170  null,
3172  );
3173 
3174  // Reset this object and the Title object
3175  $this->loadFromRow( false, self::READ_LATEST );
3176 
3177  // Search engine
3178  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3179  }
3180 
3212  public function doRollback(
3213  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3214  ) {
3215  $resultDetails = null;
3216 
3217  // Check permissions
3218  $permManager = MediaWikiServices::getInstance()->getPermissionManager();
3219  $editErrors = $permManager->getPermissionErrors( 'edit', $user, $this->mTitle );
3220  $rollbackErrors = $permManager->getPermissionErrors( 'rollback', $user, $this->mTitle );
3221  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3222 
3223  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3224  $errors[] = [ 'sessionfailure' ];
3225  }
3226 
3227  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3228  $errors[] = [ 'actionthrottledtext' ];
3229  }
3230 
3231  // If there were errors, bail out now
3232  if ( !empty( $errors ) ) {
3233  return $errors;
3234  }
3235 
3236  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3237  }
3238 
3261  public function commitRollback( $fromP, $summary, $bot,
3262  &$resultDetails, User $guser, $tags = null
3263  ) {
3265 
3266  $dbw = wfGetDB( DB_MASTER );
3267 
3268  if ( wfReadOnly() ) {
3269  return [ [ 'readonlytext' ] ];
3270  }
3271 
3272  // Begin revision creation cycle by creating a PageUpdater.
3273  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3274  $updater = $this->newPageUpdater( $guser );
3275  $current = $updater->grabParentRevision();
3276 
3277  if ( $current === null ) {
3278  // Something wrong... no page?
3279  return [ [ 'notanarticle' ] ];
3280  }
3281 
3282  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3283  $legacyCurrentCallback = function () use ( $current ) {
3284  // Only created when needed
3285  return new Revision( $current );
3286  };
3287  $from = str_replace( '_', ' ', $fromP );
3288 
3289  // User name given should match up with the top revision.
3290  // If the revision's user is not visible, then $from should be empty.
3291  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3292  $resultDetails = new DeprecatablePropertyArray(
3293  [
3294  'current' => $legacyCurrentCallback,
3295  'current-revision-record' => $current,
3296  ],
3297  [ 'current' => '1.35' ],
3298  __METHOD__
3299  );
3300  return [ [ 'alreadyrolled',
3301  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3302  htmlspecialchars( $fromP ),
3303  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3304  ] ];
3305  }
3306 
3307  // Get the last edit not by this person...
3308  // Note: these may not be public values
3309  $actorWhere = ActorMigration::newMigration()->getWhere(
3310  $dbw,
3311  'rev_user',
3312  $current->getUser( RevisionRecord::RAW )
3313  );
3314 
3315  $s = $dbw->selectRow(
3316  [ 'revision' ] + $actorWhere['tables'],
3317  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3318  [
3319  'rev_page' => $current->getPageId(),
3320  'NOT(' . $actorWhere['conds'] . ')',
3321  ],
3322  __METHOD__,
3323  [
3324  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3325  'ORDER BY' => [ 'rev_timestamp DESC', 'rev_id DESC' ]
3326  ],
3327  $actorWhere['joins']
3328  );
3329  if ( $s === false ) {
3330  // No one else ever edited this page
3331  return [ [ 'cantrollback' ] ];
3332  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3333  || $s->rev_deleted & RevisionRecord::DELETED_USER
3334  ) {
3335  // Only admins can see this text
3336  return [ [ 'notvisiblerev' ] ];
3337  }
3338 
3339  // Generate the edit summary if necessary
3340  $target = $this->getRevisionStore()->getRevisionById(
3341  $s->rev_id,
3342  RevisionStore::READ_LATEST
3343  );
3344  if ( empty( $summary ) ) {
3345  if ( !$currentEditorForPublic ) { // no public user name
3346  $summary = wfMessage( 'revertpage-nouser' );
3347  } elseif ( $wgDisableAnonTalk && $current->getUser() === 0 ) {
3348  $summary = wfMessage( 'revertpage-anon' );
3349  } else {
3350  $summary = wfMessage( 'revertpage' );
3351  }
3352  }
3353  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3354 
3355  // Allow the custom summary to use the same args as the default message
3356  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3357  $args = [
3358  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3359  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3360  $s->rev_id,
3361  $contLang->timeanddate( MWTimestamp::convert( TS_MW, $s->rev_timestamp ) ),
3362  $current->getId(),
3363  $contLang->timeanddate( $current->getTimestamp() )
3364  ];
3365  if ( $summary instanceof Message ) {
3366  $summary = $summary->params( $args )->inContentLanguage()->text();
3367  } else {
3368  $summary = wfMsgReplaceArgs( $summary, $args );
3369  }
3370 
3371  // Trim spaces on user supplied text
3372  $summary = trim( $summary );
3373 
3374  // Save
3375  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3376 
3377  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
3378  if ( $permissionManager->userHasRight( $guser, 'minoredit' ) ) {
3379  $flags |= EDIT_MINOR;
3380  }
3381 
3382  if ( $bot && ( $permissionManager->userHasAnyRight( $guser, 'markbotedits', 'bot' ) ) ) {
3383  $flags |= EDIT_FORCE_BOT;
3384  }
3385 
3386  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3387  $currentContent = $current->getContent( SlotRecord::MAIN );
3388  $targetContent = $target->getContent( SlotRecord::MAIN );
3389  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3390 
3391  // Build rollback revision:
3392  // Restore old content
3393  // TODO: MCR: test this once we can store multiple slots
3394  foreach ( $target->getSlots()->getSlots() as $slot ) {
3395  $updater->inheritSlot( $slot );
3396  }
3397 
3398  // Remove extra slots
3399  // TODO: MCR: test this once we can store multiple slots
3400  foreach ( $current->getSlotRoles() as $role ) {
3401  if ( !$target->hasSlot( $role ) ) {
3402  $updater->removeSlot( $role );
3403  }
3404  }
3405 
3406  $updater->setOriginalRevisionId( $target->getId() );
3407  $oldestRevertedRevision = $this->getRevisionStore()->getNextRevision(
3408  $target,
3409  RevisionStore::READ_LATEST
3410  );
3411  if ( $oldestRevertedRevision !== null ) {
3412  $updater->markAsRevert(
3413  EditResult::REVERT_ROLLBACK,
3414  $oldestRevertedRevision->getId(),
3415  $current->getId()
3416  );
3417  }
3418 
3419  // TODO: this logic should not be in the storage layer, it's here for compatibility
3420  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3421  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3422 
3423  if ( $wgUseRCPatrol && $permissionManager->userCan(
3424  'autopatrol', $guser, $this->getTitle()
3425  ) ) {
3426  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3427  }
3428 
3429  // Actually store the rollback
3430  $rev = $updater->saveRevision(
3432  $flags
3433  );
3434 
3435  $tags = array_merge(
3436  $tags ?: [],
3437  $updater->getEditResult()->getRevertTags()
3438  );
3439 
3440  // Set patrolling and bot flag on the edits, which gets rollbacked.
3441  // This is done even on edit failure to have patrolling in that case (T64157).
3442  $set = [];
3443  if ( $bot && $permissionManager->userHasRight( $guser, 'markbotedits' ) ) {
3444  // Mark all reverted edits as bot
3445  $set['rc_bot'] = 1;
3446  }
3447 
3448  if ( $wgUseRCPatrol ) {
3449  // Mark all reverted edits as patrolled
3450  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3451  }
3452 
3453  if ( count( $set ) ) {
3454  $actorWhere = ActorMigration::newMigration()->getWhere(
3455  $dbw,
3456  'rc_user',
3457  $current->getUser( RevisionRecord::RAW ),
3458  false
3459  );
3460  $dbw->update( 'recentchanges', $set,
3461  [ /* WHERE */
3462  'rc_cur_id' => $current->getPageId(),
3463  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3464  $actorWhere['conds'], // No tables/joins are needed for rc_user
3465  ],
3466  __METHOD__
3467  );
3468  }
3469 
3470  if ( !$updater->wasSuccessful() ) {
3471  return $updater->getStatus()->getErrorsArray();
3472  }
3473 
3474  // Report if the edit was not created because it did not change the content.
3475  if ( $updater->isUnchanged() ) {
3476  $resultDetails = new DeprecatablePropertyArray(
3477  [
3478  'current' => $legacyCurrentCallback,
3479  'current-revision-record' => $current,
3480  ],
3481  [ 'current' => '1.35' ],
3482  __METHOD__
3483  );
3484  return [ [ 'alreadyrolled',
3485  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3486  htmlspecialchars( $fromP ),
3487  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3488  ] ];
3489  }
3490 
3491  if ( $changingContentModel ) {
3492  // If the content model changed during the rollback,
3493  // make sure it gets logged to Special:Log/contentmodel
3494  $log = new ManualLogEntry( 'contentmodel', 'change' );
3495  $log->setPerformer( $guser );
3496  $log->setTarget( $this->mTitle );
3497  $log->setComment( $summary );
3498  $log->setParameters( [
3499  '4::oldmodel' => $currentContent->getModel(),
3500  '5::newmodel' => $targetContent->getModel(),
3501  ] );
3502 
3503  $logId = $log->insert( $dbw );
3504  $log->publish( $logId );
3505  }
3506 
3507  $revId = $rev->getId();
3508 
3509  // Hook is hard deprecated since 1.35
3510  if ( $this->getHookContainer()->isRegistered( 'ArticleRollbackComplete' ) ) {
3511  // Only create the Revision objects if needed
3512  $legacyCurrent = new Revision( $current );
3513  $legacyTarget = new Revision( $target );
3514  $this->getHookRunner()->onArticleRollbackComplete( $this, $guser,
3515  $legacyTarget, $legacyCurrent );
3516  }
3517 
3518  $this->getHookRunner()->onRollbackComplete( $this, $guser, $target, $current );
3519 
3520  $legacyTargetCallback = function () use ( $target ) {
3521  // Only create the Revision object if needed
3522  return new Revision( $target );
3523  };
3524  $resultDetails = new DeprecatablePropertyArray(
3525  [
3526  'summary' => $summary,
3527  'current' => $legacyCurrentCallback,
3528  'current-revision-record' => $current,
3529  'target' => $legacyTargetCallback,
3530  'target-revision-record' => $target,
3531  'newid' => $revId,
3532  'tags' => $tags
3533  ],
3534  [ 'current' => '1.35', 'target' => '1.35' ],
3535  __METHOD__
3536  );
3537 
3538  // TODO: make this return a Status object and wrap $resultDetails in that.
3539  return [];
3540  }
3541 
3553  public static function onArticleCreate( Title $title ) {
3554  // TODO: move this into a PageEventEmitter service
3555 
3556  // Update existence markers on article/talk tabs...
3557  $other = $title->getOtherPage();
3558 
3559  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3560  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3561 
3562  $title->touchLinks();
3563  $title->deleteTitleProtection();
3564 
3565  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3566 
3567  // Invalidate caches of articles which include this page
3569  $title,
3570  'templatelinks',
3571  [ 'causeAction' => 'page-create' ]
3572  );
3573  JobQueueGroup::singleton()->lazyPush( $job );
3574 
3575  if ( $title->getNamespace() == NS_CATEGORY ) {
3576  // Load the Category object, which will schedule a job to create
3577  // the category table row if necessary. Checking a replica DB is ok
3578  // here, in the worst case it'll run an unnecessary recount job on
3579  // a category that probably doesn't have many members.
3580  Category::newFromTitle( $title )->getID();
3581  }
3582  }
3583 
3589  public static function onArticleDelete( Title $title ) {
3590  // TODO: move this into a PageEventEmitter service
3591 
3592  // Update existence markers on article/talk tabs...
3593  $other = $title->getOtherPage();
3594 
3595  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3596  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3597 
3598  $title->touchLinks();
3599 
3600  $services = MediaWikiServices::getInstance();
3601  $services->getLinkCache()->invalidateTitle( $title );
3602 
3604 
3605  // Messages
3606  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3607  $services->getMessageCache()->updateMessageOverride( $title, null );
3608  }
3609 
3610  // Images
3611  if ( $title->getNamespace() == NS_FILE ) {
3613  $title,
3614  'imagelinks',
3615  [ 'causeAction' => 'page-delete' ]
3616  );
3617  JobQueueGroup::singleton()->lazyPush( $job );
3618  }
3619 
3620  // User talk pages
3621  if ( $title->getNamespace() == NS_USER_TALK ) {
3622  $user = User::newFromName( $title->getText(), false );
3623  if ( $user ) {
3624  MediaWikiServices::getInstance()
3625  ->getTalkPageNotificationManager()
3626  ->removeUserHasNewMessages( $user );
3627  }
3628  }
3629 
3630  // Image redirects
3631  $services->getRepoGroup()->getLocalRepo()->invalidateImageRedirect( $title );
3632 
3633  // Purge cross-wiki cache entities referencing this page
3635  }
3636 
3646  public static function onArticleEdit(
3647  Title $title,
3648  $revRecord = null,
3649  $slotsChanged = null
3650  ) {
3651  if ( $revRecord && $revRecord instanceof Revision ) {
3652  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
3653  $revRecord = $revRecord->getRevisionRecord();
3654  }
3655 
3656  // TODO: move this into a PageEventEmitter service
3657 
3658  $jobs = [];
3659  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3660  // Invalidate caches of articles which include this page.
3661  // Only for the main slot, because only the main slot is transcluded.
3662  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3664  $title,
3665  'templatelinks',
3666  [ 'causeAction' => 'page-edit' ]
3667  );
3668  }
3669  // Invalidate the caches of all pages which redirect here
3671  $title,
3672  'redirect',
3673  [ 'causeAction' => 'page-edit' ]
3674  );
3675  JobQueueGroup::singleton()->lazyPush( $jobs );
3676 
3677  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3678 
3679  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3680  $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3681 
3682  // Purge ?action=info cache
3683  $revid = $revRecord ? $revRecord->getId() : null;
3684  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3686  } );
3687 
3688  // Purge cross-wiki cache entities referencing this page
3690  }
3691 
3699  private static function purgeInterwikiCheckKey( Title $title ) {
3701 
3702  if ( !$wgEnableScaryTranscluding ) {
3703  return; // @todo: perhaps this wiki is only used as a *source* for content?
3704  }
3705 
3706  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3707  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3708  $cache->resetCheckKey(
3709  // Do not include the namespace since there can be multiple aliases to it
3710  // due to different namespace text definitions on different wikis. This only
3711  // means that some cache invalidations happen that are not strictly needed.
3712  $cache->makeGlobalKey(
3713  'interwiki-page',
3715  $title->getDBkey()
3716  )
3717  );
3718  } );
3719  }
3720 
3727  public function getCategories() {
3728  $id = $this->getId();
3729  if ( $id == 0 ) {
3730  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3731  }
3732 
3733  $dbr = wfGetDB( DB_REPLICA );
3734  $res = $dbr->select( 'categorylinks',
3735  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3736  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3737  // as not being aliases, and NS_CATEGORY is numeric
3738  [ 'cl_from' => $id ],
3739  __METHOD__ );
3740 
3741  return TitleArray::newFromResult( $res );
3742  }
3743 
3750  public function getHiddenCategories() {
3751  $result = [];
3752  $id = $this->getId();
3753 
3754  if ( $id == 0 ) {
3755  return [];
3756  }
3757 
3758  $dbr = wfGetDB( DB_REPLICA );
3759  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3760  [ 'cl_to' ],
3761  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3762  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3763  __METHOD__ );
3764 
3765  if ( $res !== false ) {
3766  foreach ( $res as $row ) {
3767  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3768  }
3769  }
3770 
3771  return $result;
3772  }
3773 
3781  public function getAutoDeleteReason( &$hasHistory ) {
3782  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3783  }
3784 
3795  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3796  $id = $id ?: $this->getId();
3797  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3798  getCategoryLinkType( $this->getTitle()->getNamespace() );
3799 
3800  $addFields = [ 'cat_pages = cat_pages + 1' ];
3801  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3802  if ( $type !== 'page' ) {
3803  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3804  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3805  }
3806 
3807  $dbw = wfGetDB( DB_MASTER );
3808 
3809  if ( count( $added ) ) {
3810  $existingAdded = $dbw->selectFieldValues(
3811  'category',
3812  'cat_title',
3813  [ 'cat_title' => $added ],
3814  __METHOD__
3815  );
3816 
3817  // For category rows that already exist, do a plain
3818  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3819  // to avoid creating gaps in the cat_id sequence.
3820  if ( count( $existingAdded ) ) {
3821  $dbw->update(
3822  'category',
3823  $addFields,
3824  [ 'cat_title' => $existingAdded ],
3825  __METHOD__
3826  );
3827  }
3828 
3829  $missingAdded = array_diff( $added, $existingAdded );
3830  if ( count( $missingAdded ) ) {
3831  $insertRows = [];
3832  foreach ( $missingAdded as $cat ) {
3833  $insertRows[] = [
3834  'cat_title' => $cat,
3835  'cat_pages' => 1,
3836  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3837  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3838  ];
3839  }
3840  $dbw->upsert(
3841  'category',
3842  $insertRows,
3843  'cat_title',
3844  $addFields,
3845  __METHOD__
3846  );
3847  }
3848  }
3849 
3850  if ( count( $deleted ) ) {
3851  $dbw->update(
3852  'category',
3853  $removeFields,
3854  [ 'cat_title' => $deleted ],
3855  __METHOD__
3856  );
3857  }
3858 
3859  foreach ( $added as $catName ) {
3860  $cat = Category::newFromName( $catName );
3861  $this->getHookRunner()->onCategoryAfterPageAdded( $cat, $this );
3862  }
3863 
3864  foreach ( $deleted as $catName ) {
3865  $cat = Category::newFromName( $catName );
3866  $this->getHookRunner()->onCategoryAfterPageRemoved( $cat, $this, $id );
3867  // Refresh counts on categories that should be empty now (after commit, T166757)
3868  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3869  $cat->refreshCountsIfEmpty();
3870  } );
3871  }
3872  }
3873 
3880  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3881  if ( wfReadOnly() ) {
3882  return;
3883  }
3884 
3885  if ( !$this->getHookRunner()->onOpportunisticLinksUpdate( $this,
3886  $this->mTitle, $parserOutput )
3887  ) {
3888  return;
3889  }
3890 
3891  $config = RequestContext::getMain()->getConfig();
3892 
3893  $params = [
3894  'isOpportunistic' => true,
3895  'rootJobTimestamp' => $parserOutput->getCacheTime()
3896  ];
3897 
3898  if ( $this->mTitle->areRestrictionsCascading() ) {
3899  // If the page is cascade protecting, the links should really be up-to-date
3900  JobQueueGroup::singleton()->lazyPush(
3901  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3902  );
3903  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3904  // Assume the output contains "dynamic" time/random based magic words.
3905  // Only update pages that expired due to dynamic content and NOT due to edits
3906  // to referenced templates/files. When the cache expires due to dynamic content,
3907  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3908  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3909  // template/file edit already triggered recursive RefreshLinksJob jobs.
3910  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3911  // If a page is uncacheable, do not keep spamming a job for it.
3912  // Although it would be de-duplicated, it would still waste I/O.
3914  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3915  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3916  if ( $cache->add( $key, time(), $ttl ) ) {
3917  JobQueueGroup::singleton()->lazyPush(
3918  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3919  );
3920  }
3921  }
3922  }
3923  }
3924 
3934  public function getDeletionUpdates( $rev = null ) {
3935  if ( !$rev ) {
3936  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3937 
3938  try {
3939  $rev = $this->getRevisionRecord();
3940  } catch ( Exception $ex ) {
3941  // If we can't load the content, something is wrong. Perhaps that's why
3942  // the user is trying to delete the page, so let's not fail in that case.
3943  // Note that doDeleteArticleReal() will already have logged an issue with
3944  // loading the content.
3945  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3946  }
3947  }
3948 
3949  if ( !$rev ) {
3950  $slotContent = [];
3951  } elseif ( $rev instanceof Content ) {
3952  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3953 
3954  $slotContent = [ SlotRecord::MAIN => $rev ];
3955  } else {
3956  $slotContent = array_map( function ( SlotRecord $slot ) {
3957  return $slot->getContent();
3958  }, $rev->getSlots()->getSlots() );
3959  }
3960 
3961  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3962 
3963  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3964  // model here, not the content object!
3965  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3967  foreach ( $slotContent as $role => $content ) {
3968  $handler = $content->getContentHandler();
3969 
3970  $updates = $handler->getDeletionUpdates(
3971  $this->getTitle(),
3972  $role
3973  );
3974  $allUpdates = array_merge( $allUpdates, $updates );
3975 
3976  // TODO: remove B/C hack in 1.32!
3977  $legacyUpdates = $content->getDeletionUpdates( $this );
3978 
3979  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3980  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3981  return !( $update instanceof LinksDeletionUpdate );
3982  } );
3983 
3984  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3985  }
3986 
3987  $this->getHookRunner()->onPageDeletionDataUpdates(
3988  $this->getTitle(), $rev, $allUpdates );
3989 
3990  // TODO: hard deprecate old hook in 1.33
3991  $this->getHookRunner()->onWikiPageDeletionUpdates( $this, $content, $allUpdates );
3992  return $allUpdates;
3993  }
3994 
4002  public function isLocal() {
4003  return true;
4004  }
4005 
4015  public function getWikiDisplayName() {
4016  global $wgSitename;
4017  return $wgSitename;
4018  }
4019 
4028  public function getSourceURL() {
4029  return $this->getTitle()->getCanonicalURL();
4030  }
4031 
4038  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
4039 
4040  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
4041  }
4042 
4043 }
WikiPage\hasDifferencesOutsideMainSlot
static hasDifferencesOutsideMainSlot( $a, $b)
Helper method for checking whether two revisions have differences that go beyond the main slot.
Definition: WikiPage.php:1562
WikiPage\getCategories
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:3727
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:44
$wgUseAutomaticEditSummaries
$wgUseAutomaticEditSummaries
If user doesn't specify any edit summary when making a an edit, MediaWiki will try to automatically c...
Definition: DefaultSettings.php:7098
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
Page
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:30
WikiPage\getComment
getComment( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:922
CacheTime\getCacheExpiry
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:129
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:557
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:3553
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
WikiPage\loadPageData
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:465
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
WikiPage\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3781
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:781
ParserOutput
Definition: ParserOutput.php:25
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
WikiPage\getRedirectTarget
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:1015
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:272
User\getId
getId()
Get the user's ID.
Definition: User.php:2042
WikiPage\clearCacheFields
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:337
Title\getFragment
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1739
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:2686
$wgPageCreationLog
$wgPageCreationLog
Maintain a log of page creations at Special:Log/create?
Definition: DefaultSettings.php:8348
WikiPage\updateRevisionOn
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1394
WikiPage\wasLoadedFrom
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:510
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:1591
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:152
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:145
WikiPage\getCreator
getCreator( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:868
EDIT_INTERNAL
const EDIT_INTERNAL
Definition: Defines.php:148
MediaWiki\Storage\DerivedPageDataUpdater\setArticleCountMethod
setArticleCountMethod( $articleCountMethod)
Definition: DerivedPageDataUpdater.php:440
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:542
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:80
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:1846
WikiPage\hasViewableContent
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition: WikiPage.php:603
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:674
WikiPage\doViewUpdates
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1291
MediaWiki\Storage\DerivedPageDataUpdater\setRcWatchCategoryMembership
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
Definition: DerivedPageDataUpdater.php:448
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1677
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1723
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:442
SearchUpdate
Database independant search index updater.
Definition: SearchUpdate.php:33
WikiPage\$mDataLoadedFrom
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:99
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred update queue for execution at the appropriate time.
Definition: DeferredUpdates.php:106
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:51
WikiPage\replaceSectionContent
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1639
PoolWorkArticleView
Definition: PoolWorkArticleView.php:28
NS_FILE
const NS_FILE
Definition: Defines.php:75
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2013
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1139
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1128
wfMsgReplaceArgs
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
Definition: GlobalFunctions.php:1257
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:533
WikiPage\getRevision
getRevision()
Get the latest revision.
Definition: WikiPage.php:768
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:1222
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:868
$s
$s
Definition: mergeMessageFileList.php:185
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:1177
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:589
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1089
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
User\pingLimiter
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1760
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
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:2978
WikiPage\getSlotRoleRegistry
getSlotRoleRegistry()
Definition: WikiPage.php:265
WikiPage\getDBLoadBalancer
getDBLoadBalancer()
Definition: WikiPage.php:286
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:296
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:7329
$revQuery
$revQuery
Definition: testCompression.php:56
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:7345
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:139
HTMLCacheUpdateJob\newForBacklinks
static newForBacklinks(Title $title, $table, $params=[])
Definition: HTMLCacheUpdateJob.php:59
WikiPage\$mTitle
Title $mTitle
Definition: WikiPage.php:61
WikiPage\$mTouched
string $mTouched
Definition: WikiPage.php:119
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
WikiPage\getUserText
getUserText( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:894
WikiPage\triggerOpportunisticLinksUpdate
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3880
WikiPage\protectDescription
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2618
WikiPage\$mLastRevision
RevisionRecord $mLastRevision
Definition: WikiPage.php:109
$dbr
$dbr
Definition: testCompression.php:54
IDBAccessObject\READ_LOCKING
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:64
Revision
Definition: Revision.php:39
User\matchEditToken
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:3885
WikiPage\updateParserCache
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2155
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1621
$wgEnableScaryTranscluding
$wgEnableScaryTranscluding
Enable interwiki transcluding.
Definition: DefaultSettings.php:4727
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:1023
WikiCategoryPage
Special handling for category pages.
Definition: WikiCategoryPage.php:26
MWException
MediaWiki exception.
Definition: MWException.php:26
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:154
WikiPage\doDeleteArticleBatched
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2806
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:945
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1029
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
WikiPage\doRollback
doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user, $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:3212
MediaWiki\Storage\DerivedPageDataUpdater\setLogger
setLogger(LoggerInterface $logger)
Definition: DerivedPageDataUpdater.php:327
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1032
wfArrayDiff2
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
Definition: GlobalFunctions.php:112
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:524
WikiPage\doSecondaryDataUpdates
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2199
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2464
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:357
Title\getInterwiki
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:933
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1249
WikiPage\getId
getId()
Definition: WikiPage.php:578
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1349
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1227
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:318
MediaWiki\Content\ContentHandlerFactory
Definition: ContentHandlerFactory.php:44
WikiPage\exists
exists()
Definition: WikiPage.php:588
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:142
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3589
ObjectCache\getInstance
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:78
$args
if( $line===false) $args
Definition: mcc.php:124
WikiPage\$mRedirectTarget
Title $mRedirectTarget
Definition: WikiPage.php:104
WikiPage\__construct
__construct(Title $title)
Definition: WikiPage.php:134
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:663
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:85
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:685
WikiPage\purgeInterwikiCheckKey
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3699
WikiPage\$mDataLoaded
bool $mDataLoaded
Definition: WikiPage.php:68
$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:3123
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:592
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:827
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:696
ParserOptions\getStubThreshold
getStubThreshold()
Thumb size preferred by the user.
Definition: ParserOptions.php:558
WikiPage\insertNullProtectionRevision
insertNullProtectionRevision(string $revCommentMsg, array $limit, array $expiry, bool $cascade, string $reason, User $user)
Insert a new null revision for this page.
Definition: WikiPage.php:2536
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:608
DB_MASTER
const DB_MASTER
Definition: defines.php:26
$revStore
$revStore
Definition: testCompression.php:55
IDBAccessObject\READ_NONE
const READ_NONE
Definition: IDBAccessObject.php:73
WikiPage\$mLatest
int false $mLatest
False means "not loaded".
Definition: WikiPage.php:82
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:912
WikiPage\doPurge
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1314
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:962
User\clearNotification
clearNotification(&$title, $oldid=0)
Clear the user's notification timestamp for the given title.
Definition: User.php:3305
WikiPage\getContentModel
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:630
MediaWiki\Storage\EditResult
Object for storing information about the effects of an edit.
Definition: EditResult.php:36
WikiPage\pageDataFromTitle
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object's namespace and title using a sanitized title string.
Definition: WikiPage.php:435
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:3094
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:9159
Category\newFromTitle
static newFromTitle( $title)
Factory function.
Definition: Category.php:153
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:1790
WikiPage\updateIfNewerOn
updateIfNewerOn( $dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record.
Definition: WikiPage.php:1512
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
$content
$content
Definition: router.php:76
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:72
WikiPage\getDeletionUpdates
getDeletionUpdates( $rev=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3934
WikiPage\protectDescriptionLog
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2660
WikiPage\insertRedirect
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1062
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:142
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:57
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
WikiPage\doEditUpdates
doEditUpdates( $revisionRecord, User $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2116
MediaWiki\Storage\PageUpdater
Controller-like object for creating and updating pages by creating new revisions.
Definition: PageUpdater.php:77
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
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:192
WikiPage\insertProtectNullRevision
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2505
$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:4028
ParserOptions\newCanonical
static newCanonical( $context=null, $userLang=null)
Creates a "canonical" ParserOptions object.
Definition: ParserOptions.php:1135
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1490
WikiPage\$derivedDataUpdater
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:129
LinksDeletionUpdate
Update object handling the cleanup of links tables after a page was deleted.
Definition: LinksDeletionUpdate.php:28
WikiPage\commitRollback
commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser, $tags=null)
Backend implementation of doRollback(), please refer there for parameter and return value documentati...
Definition: WikiPage.php:3261
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3750
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:222
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:83
$wgDeleteRevisionsBatchSize
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
Definition: DefaultSettings.php:5958
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:451
WikiPage\doEditContent
doEditContent(Content $content, $summary, $flags=0, $originalRevId=false, User $user=null, $serialFormat=null, $tags=[], $undidRevId=0)
Change an existing article or create a new article.
Definition: WikiPage.php:1931
WikiPage\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object.
Definition: WikiPage.php:370
WikiPage\getOldestRevision
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:709
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:719
WikiPage\setLastEdit
setLastEdit(RevisionRecord $revRecord)
Set the latest revision.
Definition: WikiPage.php:758
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1128
Content
Base interface for content objects.
Definition: Content.php:34
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:141
WikiPage\loadFromRow
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:536
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:5752
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2596
Title
Represents a title within MediaWiki.
Definition: Title.php:42
WikiPage\getUser
getUser( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:840
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:255
WikiPage\$mIsRedirect
bool $mIsRedirect
Definition: WikiPage.php:75
WikiPage\newDerivedDataUpdater
newDerivedDataUpdater()
Definition: WikiPage.php:1738
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:1141
$cache
$cache
Definition: mcc.php:33
DeferredUpdates\PRESEND
const PRESEND
Definition: DeferredUpdates.php:84
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:50
WebRequest\getRequestId
static getRequestId()
Get the unique request ID.
Definition: WebRequest.php:330
$wgAjaxEditStash
$wgAjaxEditStash
Have clients send edits to be prepared when filling in edit summaries.
Definition: DefaultSettings.php:8666
WikiPage\$mId
int $mId
Definition: WikiPage.php:94
WikiPage\doDeleteArticleReal
doDeleteArticleReal( $reason, $user=false, $suppress=false, $u2=null, &$error='', User $deleter=null, $tags=[], $logsubtype='delete', $immediate=false)
Back-end article deletion Deletes the article with database consistency, writes logs,...
Definition: WikiPage.php:2753
DeletePageJob
Class DeletePageJob.
Definition: DeletePageJob.php:6
WikiPage\getWikiDisplayName
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:4015
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:2044
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:160
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:234
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:3795
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:4037
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:38
WikiPage\pageData
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:406
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
WikiFilePage
Special handling for file pages.
Definition: WikiFilePage.php:31
WikiPage\isLocal
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:4002
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1076
$wgArticleCountMethod
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article.
Definition: DefaultSettings.php:4770
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:77
Category\newFromName
static newFromName( $name)
Factory function.
Definition: Category.php:133
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:143
ParserOptions\isSafeToCache
isSafeToCache()
Test whether these options are safe to cache.
Definition: ParserOptions.php:1462
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:1088
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:813
WikiPage\getRevisionRenderer
getRevisionRenderer()
Definition: WikiPage.php:258
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1475
WikiPage\$mPreparedEdit
PreparedEdit false $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:89
Revision\getRevisionRecord
getRevisionRecord()
Definition: Revision.php:446
WikiPage\$mLinksUpdated
string $mLinksUpdated
Definition: WikiPage.php:124
WikiPage\onArticleEdit
static onArticleEdit(Title $title, $revRecord=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3646
WikiPage\doDeleteArticle
doDeleteArticle( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $user=null, $immediate=false)
Same as doDeleteArticleReal(), but returns a simple boolean.
Definition: WikiPage.php:2717
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:612
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:103
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:60
CommentStore\getStore
static getStore()
Definition: CommentStore.php:109
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:54
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:145
ParserOutput\hasDynamicContent
hasDynamicContent()
Check whether the cache TTL was lowered due to dynamic content.
Definition: ParserOutput.php:1337
WikiPage\getParserCache
getParserCache()
Definition: WikiPage.php:279
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:449
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:309
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:802
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2071
CommentStoreComment
CommentStoreComment represents a comment stored by CommentStore.
Definition: CommentStoreComment.php:29
WikiPage\$mTimestamp
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition: WikiPage.php:114
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:3064
$wgDisableAnonTalk
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
Definition: DefaultSettings.php:7442
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
$wgRCWatchCategoryMembership
$wgRCWatchCategoryMembership
Treat category membership changes as a RecentChange.
Definition: DefaultSettings.php:7319
WikiPage\getContentHandlerFactory
getContentHandlerFactory()
Definition: WikiPage.php:272
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:326
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:2229
WikiPage\getRevisionStore
getRevisionStore()
Definition: WikiPage.php:251
$type
$type
Definition: testCompression.php:52