MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
38 
47 class WikiPage implements Page, IDBAccessObject {
48  // Constants for $mDataLoadedFrom and related
49 
53  public $mTitle = null;
54 
59  public $mDataLoaded = false;
60 
65  public $mIsRedirect = false;
66 
71  public $mLatest = false;
72 
76  public $mPreparedEdit = false;
77 
81  protected $mId = null;
82 
86  protected $mDataLoadedFrom = self::READ_NONE;
87 
91  protected $mRedirectTarget = null;
92 
96  protected $mLastRevision = null;
97 
101  protected $mTimestamp = '';
102 
106  protected $mTouched = '19700101000000';
107 
111  protected $mLinksUpdated = '19700101000000';
112 
116  private $derivedDataUpdater = null;
117 
122  public function __construct( Title $title ) {
123  $this->mTitle = $title;
124  }
125 
130  public function __clone() {
131  $this->mTitle = clone $this->mTitle;
132  }
133 
142  public static function factory( Title $title ) {
143  $ns = $title->getNamespace();
144 
145  if ( $ns == NS_MEDIA ) {
146  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
147  } elseif ( $ns < 0 ) {
148  throw new MWException( "Invalid or virtual namespace $ns given." );
149  }
150 
151  $page = null;
152  if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
153  return $page;
154  }
155 
156  switch ( $ns ) {
157  case NS_FILE:
158  $page = new WikiFilePage( $title );
159  break;
160  case NS_CATEGORY:
161  $page = new WikiCategoryPage( $title );
162  break;
163  default:
164  $page = new WikiPage( $title );
165  }
166 
167  return $page;
168  }
169 
180  public static function newFromID( $id, $from = 'fromdb' ) {
181  // page ids are never 0 or negative, see T63166
182  if ( $id < 1 ) {
183  return null;
184  }
185 
186  $from = self::convertSelectType( $from );
187  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
188  $pageQuery = self::getQueryInfo();
189  $row = $db->selectRow(
190  $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
191  [], $pageQuery['joins']
192  );
193  if ( !$row ) {
194  return null;
195  }
196  return self::newFromRow( $row, $from );
197  }
198 
210  public static function newFromRow( $row, $from = 'fromdb' ) {
211  $page = self::factory( Title::newFromRow( $row ) );
212  $page->loadFromRow( $row, $from );
213  return $page;
214  }
215 
222  protected static function convertSelectType( $type ) {
223  switch ( $type ) {
224  case 'fromdb':
225  return self::READ_NORMAL;
226  case 'fromdbmaster':
227  return self::READ_LATEST;
228  case 'forupdate':
229  return self::READ_LOCKING;
230  default:
231  // It may already be an integer or whatever else
232  return $type;
233  }
234  }
235 
239  private function getRevisionStore() {
240  return MediaWikiServices::getInstance()->getRevisionStore();
241  }
242 
246  private function getRevisionRenderer() {
247  return MediaWikiServices::getInstance()->getRevisionRenderer();
248  }
249 
253  private function getSlotRoleRegistry() {
254  return MediaWikiServices::getInstance()->getSlotRoleRegistry();
255  }
256 
260  private function getParserCache() {
261  return MediaWikiServices::getInstance()->getParserCache();
262  }
263 
267  private function getDBLoadBalancer() {
268  return MediaWikiServices::getInstance()->getDBLoadBalancer();
269  }
270 
277  public function getActionOverrides() {
278  return $this->getContentHandler()->getActionOverrides();
279  }
280 
290  public function getContentHandler() {
292  }
293 
298  public function getTitle() {
299  return $this->mTitle;
300  }
301 
306  public function clear() {
307  $this->mDataLoaded = false;
308  $this->mDataLoadedFrom = self::READ_NONE;
309 
310  $this->clearCacheFields();
311  }
312 
317  protected function clearCacheFields() {
318  $this->mId = null;
319  $this->mRedirectTarget = null; // Title object if set
320  $this->mLastRevision = null; // Latest revision
321  $this->mTouched = '19700101000000';
322  $this->mLinksUpdated = '19700101000000';
323  $this->mTimestamp = '';
324  $this->mIsRedirect = false;
325  $this->mLatest = false;
326  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
327  // checks the requested rev ID and content against the cached one. For most
328  // content types, the output should not change during the lifetime of this cache.
329  // Clearing it can cause extra parses on edit for no reason.
330  }
331 
337  public function clearPreparedEdit() {
338  $this->mPreparedEdit = false;
339  }
340 
348  public static function selectFields() {
350 
351  wfDeprecated( __METHOD__, '1.31' );
352 
353  $fields = [
354  'page_id',
355  'page_namespace',
356  'page_title',
357  'page_restrictions',
358  'page_is_redirect',
359  'page_is_new',
360  'page_random',
361  'page_touched',
362  'page_links_updated',
363  'page_latest',
364  'page_len',
365  ];
366 
367  if ( $wgContentHandlerUseDB ) {
368  $fields[] = 'page_content_model';
369  }
370 
371  if ( $wgPageLanguageUseDB ) {
372  $fields[] = 'page_lang';
373  }
374 
375  return $fields;
376  }
377 
387  public static function getQueryInfo() {
389 
390  $ret = [
391  'tables' => [ 'page' ],
392  'fields' => [
393  'page_id',
394  'page_namespace',
395  'page_title',
396  'page_restrictions',
397  'page_is_redirect',
398  'page_is_new',
399  'page_random',
400  'page_touched',
401  'page_links_updated',
402  'page_latest',
403  'page_len',
404  ],
405  'joins' => [],
406  ];
407 
408  if ( $wgContentHandlerUseDB ) {
409  $ret['fields'][] = 'page_content_model';
410  }
411 
412  if ( $wgPageLanguageUseDB ) {
413  $ret['fields'][] = 'page_lang';
414  }
415 
416  return $ret;
417  }
418 
426  protected function pageData( $dbr, $conditions, $options = [] ) {
427  $pageQuery = self::getQueryInfo();
428 
429  // Avoid PHP 7.1 warning of passing $this by reference
430  $wikiPage = $this;
431 
432  Hooks::run( 'ArticlePageDataBefore', [
433  &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
434  ] );
435 
436  $row = $dbr->selectRow(
437  $pageQuery['tables'],
438  $pageQuery['fields'],
439  $conditions,
440  __METHOD__,
441  $options,
442  $pageQuery['joins']
443  );
444 
445  Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
446 
447  return $row;
448  }
449 
459  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
460  return $this->pageData( $dbr, [
461  'page_namespace' => $title->getNamespace(),
462  'page_title' => $title->getDBkey() ], $options );
463  }
464 
473  public function pageDataFromId( $dbr, $id, $options = [] ) {
474  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
475  }
476 
489  public function loadPageData( $from = 'fromdb' ) {
490  $from = self::convertSelectType( $from );
491  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
492  // We already have the data from the correct location, no need to load it twice.
493  return;
494  }
495 
496  if ( is_int( $from ) ) {
497  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
498  $loadBalancer = $this->getDBLoadBalancer();
499  $db = $loadBalancer->getConnection( $index );
500  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
501 
502  if ( !$data
503  && $index == DB_REPLICA
504  && $loadBalancer->getServerCount() > 1
505  && $loadBalancer->hasOrMadeRecentMasterChanges()
506  ) {
507  $from = self::READ_LATEST;
508  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
509  $db = $loadBalancer->getConnection( $index );
510  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
511  }
512  } else {
513  // No idea from where the caller got this data, assume replica DB.
514  $data = $from;
515  $from = self::READ_NORMAL;
516  }
517 
518  $this->loadFromRow( $data, $from );
519  }
520 
534  public function wasLoadedFrom( $from ) {
535  $from = self::convertSelectType( $from );
536 
537  if ( !is_int( $from ) ) {
538  // No idea from where the caller got this data, assume replica DB.
539  $from = self::READ_NORMAL;
540  }
541 
542  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
543  return true;
544  }
545 
546  return false;
547  }
548 
560  public function loadFromRow( $data, $from ) {
561  $lc = MediaWikiServices::getInstance()->getLinkCache();
562  $lc->clearLink( $this->mTitle );
563 
564  if ( $data ) {
565  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
566 
567  $this->mTitle->loadFromRow( $data );
568 
569  // Old-fashioned restrictions
570  $this->mTitle->loadRestrictions( $data->page_restrictions );
571 
572  $this->mId = intval( $data->page_id );
573  $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
574  $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
575  $this->mIsRedirect = intval( $data->page_is_redirect );
576  $this->mLatest = intval( $data->page_latest );
577  // T39225: $latest may no longer match the cached latest Revision object.
578  // Double-check the ID of any cached latest Revision object for consistency.
579  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
580  $this->mLastRevision = null;
581  $this->mTimestamp = '';
582  }
583  } else {
584  $lc->addBadLinkObj( $this->mTitle );
585 
586  $this->mTitle->loadFromRow( false );
587 
588  $this->clearCacheFields();
589 
590  $this->mId = 0;
591  }
592 
593  $this->mDataLoaded = true;
594  $this->mDataLoadedFrom = self::convertSelectType( $from );
595  }
596 
600  public function getId() {
601  if ( !$this->mDataLoaded ) {
602  $this->loadPageData();
603  }
604  return $this->mId;
605  }
606 
610  public function exists() {
611  if ( !$this->mDataLoaded ) {
612  $this->loadPageData();
613  }
614  return $this->mId > 0;
615  }
616 
625  public function hasViewableContent() {
626  return $this->mTitle->isKnown();
627  }
628 
634  public function isRedirect() {
635  if ( !$this->mDataLoaded ) {
636  $this->loadPageData();
637  }
638 
639  return (bool)$this->mIsRedirect;
640  }
641 
652  public function getContentModel() {
653  if ( $this->exists() ) {
654  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
655 
656  return $cache->getWithSetCallback(
657  $cache->makeKey( 'page-content-model', $this->getLatest() ),
658  $cache::TTL_MONTH,
659  function () {
660  $rev = $this->getRevision();
661  if ( $rev ) {
662  // Look at the revision's actual content model
663  return $rev->getContentModel();
664  } else {
665  $title = $this->mTitle->getPrefixedDBkey();
666  wfWarn( "Page $title exists but has no (visible) revisions!" );
667  return $this->mTitle->getContentModel();
668  }
669  }
670  );
671  }
672 
673  // use the default model for this page
674  return $this->mTitle->getContentModel();
675  }
676 
681  public function checkTouched() {
682  if ( !$this->mDataLoaded ) {
683  $this->loadPageData();
684  }
685  return ( $this->mId && !$this->mIsRedirect );
686  }
687 
692  public function getTouched() {
693  if ( !$this->mDataLoaded ) {
694  $this->loadPageData();
695  }
696  return $this->mTouched;
697  }
698 
703  public function getLinksTimestamp() {
704  if ( !$this->mDataLoaded ) {
705  $this->loadPageData();
706  }
707  return $this->mLinksUpdated;
708  }
709 
714  public function getLatest() {
715  if ( !$this->mDataLoaded ) {
716  $this->loadPageData();
717  }
718  return (int)$this->mLatest;
719  }
720 
725  public function getOldestRevision() {
726  // Try using the replica DB first, then try the master
727  $rev = $this->mTitle->getFirstRevision();
728  if ( !$rev ) {
729  $rev = $this->mTitle->getFirstRevision( Title::READ_LATEST );
730  }
731  return $rev;
732  }
733 
738  protected function loadLastEdit() {
739  if ( $this->mLastRevision !== null ) {
740  return; // already loaded
741  }
742 
743  $latest = $this->getLatest();
744  if ( !$latest ) {
745  return; // page doesn't exist or is missing page_latest info
746  }
747 
748  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
749  // T39225: if session S1 loads the page row FOR UPDATE, the result always
750  // includes the latest changes committed. This is true even within REPEATABLE-READ
751  // transactions, where S1 normally only sees changes committed before the first S1
752  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
753  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
754  // happened after the first S1 SELECT.
755  // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
756  $flags = Revision::READ_LOCKING;
757  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
758  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
759  // Bug T93976: if page_latest was loaded from the master, fetch the
760  // revision from there as well, as it may not exist yet on a replica DB.
761  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
762  $flags = Revision::READ_LATEST;
763  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
764  } else {
765  $dbr = wfGetDB( DB_REPLICA );
766  $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
767  }
768 
769  if ( $revision ) { // sanity
770  $this->setLastEdit( $revision );
771  }
772  }
773 
778  protected function setLastEdit( Revision $revision ) {
779  $this->mLastRevision = $revision;
780  $this->mTimestamp = $revision->getTimestamp();
781  }
782 
787  public function getRevision() {
788  $this->loadLastEdit();
789  if ( $this->mLastRevision ) {
790  return $this->mLastRevision;
791  }
792  return null;
793  }
794 
799  public function getRevisionRecord() {
800  $this->loadLastEdit();
801  if ( $this->mLastRevision ) {
802  return $this->mLastRevision->getRevisionRecord();
803  }
804  return null;
805  }
806 
820  public function getContent( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
821  $this->loadLastEdit();
822  if ( $this->mLastRevision ) {
823  return $this->mLastRevision->getContent( $audience, $user );
824  }
825  return null;
826  }
827 
831  public function getTimestamp() {
832  // Check if the field has been filled by WikiPage::setTimestamp()
833  if ( !$this->mTimestamp ) {
834  $this->loadLastEdit();
835  }
836 
837  return wfTimestamp( TS_MW, $this->mTimestamp );
838  }
839 
845  public function setTimestamp( $ts ) {
846  $this->mTimestamp = wfTimestamp( TS_MW, $ts );
847  }
848 
858  public function getUser( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
859  $this->loadLastEdit();
860  if ( $this->mLastRevision ) {
861  return $this->mLastRevision->getUser( $audience, $user );
862  } else {
863  return -1;
864  }
865  }
866 
877  public function getCreator( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
878  $revision = $this->getOldestRevision();
879  if ( $revision ) {
880  $userName = $revision->getUserText( $audience, $user );
881  return User::newFromName( $userName, false );
882  } else {
883  return null;
884  }
885  }
886 
896  public function getUserText( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
897  $this->loadLastEdit();
898  if ( $this->mLastRevision ) {
899  return $this->mLastRevision->getUserText( $audience, $user );
900  } else {
901  return '';
902  }
903  }
904 
915  public function getComment( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
916  $this->loadLastEdit();
917  if ( $this->mLastRevision ) {
918  return $this->mLastRevision->getComment( $audience, $user );
919  } else {
920  return '';
921  }
922  }
923 
929  public function getMinorEdit() {
930  $this->loadLastEdit();
931  if ( $this->mLastRevision ) {
932  return $this->mLastRevision->isMinor();
933  } else {
934  return false;
935  }
936  }
937 
946  public function isCountable( $editInfo = false ) {
947  global $wgArticleCountMethod;
948 
949  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
950 
951  if ( !$this->mTitle->isContentPage() ) {
952  return false;
953  }
954 
955  if ( $editInfo ) {
956  // NOTE: only the main slot can make a page a redirect
957  $content = $editInfo->pstContent;
958  } else {
959  $content = $this->getContent();
960  }
961 
962  if ( !$content || $content->isRedirect() ) {
963  return false;
964  }
965 
966  $hasLinks = null;
967 
968  if ( $wgArticleCountMethod === 'link' ) {
969  // nasty special case to avoid re-parsing to detect links
970 
971  if ( $editInfo ) {
972  // ParserOutput::getLinks() is a 2D array of page links, so
973  // to be really correct we would need to recurse in the array
974  // but the main array should only have items in it if there are
975  // links.
976  $hasLinks = (bool)count( $editInfo->output->getLinks() );
977  } else {
978  // NOTE: keep in sync with RevisionRenderer::getLinkCount
979  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
980  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
981  [ 'pl_from' => $this->getId() ], __METHOD__ );
982  }
983  }
984 
985  // TODO: MCR: determine $hasLinks for each slot, and use that info
986  // with that slot's Content's isCountable method. That requires per-
987  // slot ParserOutput in the ParserCache, or per-slot info in the
988  // pagelinks table.
989  return $content->isCountable( $hasLinks );
990  }
991 
999  public function getRedirectTarget() {
1000  if ( !$this->mTitle->isRedirect() ) {
1001  return null;
1002  }
1003 
1004  if ( $this->mRedirectTarget !== null ) {
1005  return $this->mRedirectTarget;
1006  }
1007 
1008  // Query the redirect table
1009  $dbr = wfGetDB( DB_REPLICA );
1010  $row = $dbr->selectRow( 'redirect',
1011  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1012  [ 'rd_from' => $this->getId() ],
1013  __METHOD__
1014  );
1015 
1016  // rd_fragment and rd_interwiki were added later, populate them if empty
1017  if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
1018  // (T203942) We can't redirect to Media namespace because it's virtual.
1019  // We don't want to modify Title objects farther down the
1020  // line. So, let's fix this here by changing to File namespace.
1021  if ( $row->rd_namespace == NS_MEDIA ) {
1022  $namespace = NS_FILE;
1023  } else {
1024  $namespace = $row->rd_namespace;
1025  }
1026  $this->mRedirectTarget = Title::makeTitle(
1027  $namespace, $row->rd_title,
1028  $row->rd_fragment, $row->rd_interwiki
1029  );
1030  return $this->mRedirectTarget;
1031  }
1032 
1033  // This page doesn't have an entry in the redirect table
1034  $this->mRedirectTarget = $this->insertRedirect();
1035  return $this->mRedirectTarget;
1036  }
1037 
1046  public function insertRedirect() {
1047  $content = $this->getContent();
1048  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1049  if ( !$retval ) {
1050  return null;
1051  }
1052 
1053  // Update the DB post-send if the page has not cached since now
1054  $latest = $this->getLatest();
1056  function () use ( $retval, $latest ) {
1057  $this->insertRedirectEntry( $retval, $latest );
1058  },
1060  wfGetDB( DB_MASTER )
1061  );
1062 
1063  return $retval;
1064  }
1065 
1072  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1073  $dbw = wfGetDB( DB_MASTER );
1074  $dbw->startAtomic( __METHOD__ );
1075 
1076  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1077  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1078  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1079  $dbw->upsert(
1080  'redirect',
1081  [
1082  'rd_from' => $this->getId(),
1083  'rd_namespace' => $rt->getNamespace(),
1084  'rd_title' => $rt->getDBkey(),
1085  'rd_fragment' => $truncatedFragment,
1086  'rd_interwiki' => $rt->getInterwiki(),
1087  ],
1088  [ 'rd_from' ],
1089  [
1090  'rd_namespace' => $rt->getNamespace(),
1091  'rd_title' => $rt->getDBkey(),
1092  'rd_fragment' => $truncatedFragment,
1093  'rd_interwiki' => $rt->getInterwiki(),
1094  ],
1095  __METHOD__
1096  );
1097  $success = true;
1098  } else {
1099  $success = false;
1100  }
1101 
1102  $dbw->endAtomic( __METHOD__ );
1103 
1104  return $success;
1105  }
1106 
1112  public function followRedirect() {
1113  return $this->getRedirectURL( $this->getRedirectTarget() );
1114  }
1115 
1123  public function getRedirectURL( $rt ) {
1124  if ( !$rt ) {
1125  return false;
1126  }
1127 
1128  if ( $rt->isExternal() ) {
1129  if ( $rt->isLocal() ) {
1130  // Offsite wikis need an HTTP redirect.
1131  // This can be hard to reverse and may produce loops,
1132  // so they may be disabled in the site configuration.
1133  $source = $this->mTitle->getFullURL( 'redirect=no' );
1134  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1135  } else {
1136  // External pages without "local" bit set are not valid
1137  // redirect targets
1138  return false;
1139  }
1140  }
1141 
1142  if ( $rt->isSpecialPage() ) {
1143  // Gotta handle redirects to special pages differently:
1144  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1145  // Some pages are not valid targets.
1146  if ( $rt->isValidRedirectTarget() ) {
1147  return $rt->getFullURL();
1148  } else {
1149  return false;
1150  }
1151  }
1152 
1153  return $rt;
1154  }
1155 
1161  public function getContributors() {
1162  // @todo: This is expensive; cache this info somewhere.
1163 
1164  $dbr = wfGetDB( DB_REPLICA );
1165 
1166  $actorMigration = ActorMigration::newMigration();
1167  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1168 
1169  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1170 
1171  $fields = [
1172  'user_id' => $actorQuery['fields']['rev_user'],
1173  'user_name' => $actorQuery['fields']['rev_user_text'],
1174  'actor_id' => $actorQuery['fields']['rev_actor'],
1175  'user_real_name' => 'MIN(user_real_name)',
1176  'timestamp' => 'MAX(rev_timestamp)',
1177  ];
1178 
1179  $conds = [ 'rev_page' => $this->getId() ];
1180 
1181  // The user who made the top revision gets credited as "this page was last edited by
1182  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1183  $user = $this->getUser()
1184  ? User::newFromId( $this->getUser() )
1185  : User::newFromName( $this->getUserText(), false );
1186  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1187 
1188  // Username hidden?
1189  $conds[] = "{$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER )} = 0";
1190 
1191  $jconds = [
1192  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1193  ] + $actorQuery['joins'];
1194 
1195  $options = [
1196  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1197  'ORDER BY' => 'timestamp DESC',
1198  ];
1199 
1200  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1201  return new UserArrayFromResult( $res );
1202  }
1203 
1211  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1212  return $parserOptions->getStubThreshold() == 0
1213  && $this->exists()
1214  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1215  && $this->getContentHandler()->isParserCacheSupported();
1216  }
1217 
1233  public function getParserOutput(
1234  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1235  ) {
1236  $useParserCache =
1237  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1238 
1239  if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1240  throw new InvalidArgumentException(
1241  'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1242  );
1243  }
1244 
1245  wfDebug( __METHOD__ .
1246  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1247  if ( $parserOptions->getStubThreshold() ) {
1248  wfIncrStats( 'pcache.miss.stub' );
1249  }
1250 
1251  if ( $useParserCache ) {
1252  $parserOutput = $this->getParserCache()
1253  ->get( $this, $parserOptions );
1254  if ( $parserOutput !== false ) {
1255  return $parserOutput;
1256  }
1257  }
1258 
1259  if ( $oldid === null || $oldid === 0 ) {
1260  $oldid = $this->getLatest();
1261  }
1262 
1263  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1264  $pool->execute();
1265 
1266  return $pool->getParserOutput();
1267  }
1268 
1274  public function doViewUpdates( User $user, $oldid = 0 ) {
1275  if ( wfReadOnly() ) {
1276  return;
1277  }
1278 
1279  // Update newtalk / watchlist notification status;
1280  // Avoid outage if the master is not reachable by using a deferred updated
1282  function () use ( $user, $oldid ) {
1283  Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1284 
1285  $user->clearNotification( $this->mTitle, $oldid );
1286  },
1288  );
1289  }
1290 
1297  public function doPurge() {
1298  // Avoid PHP 7.1 warning of passing $this by reference
1299  $wikiPage = $this;
1300 
1301  if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1302  return false;
1303  }
1304 
1305  $this->mTitle->invalidateCache();
1306 
1307  // Clear file cache
1309  // Send purge after above page_touched update was committed
1311  new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1313  );
1314 
1315  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1316  $messageCache = MessageCache::singleton();
1317  $messageCache->updateMessageOverride( $this->mTitle, $this->getContent() );
1318  }
1319 
1320  return true;
1321  }
1322 
1339  public function insertOn( $dbw, $pageId = null ) {
1340  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1341  $dbw->insert(
1342  'page',
1343  [
1344  'page_namespace' => $this->mTitle->getNamespace(),
1345  'page_title' => $this->mTitle->getDBkey(),
1346  'page_restrictions' => '',
1347  'page_is_redirect' => 0, // Will set this shortly...
1348  'page_is_new' => 1,
1349  'page_random' => wfRandom(),
1350  'page_touched' => $dbw->timestamp(),
1351  'page_latest' => 0, // Fill this in shortly...
1352  'page_len' => 0, // Fill this in shortly...
1353  ] + $pageIdForInsert,
1354  __METHOD__,
1355  [ 'IGNORE' ]
1356  );
1357 
1358  if ( $dbw->affectedRows() > 0 ) {
1359  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1360  $this->mId = $newid;
1361  $this->mTitle->resetArticleID( $newid );
1362 
1363  return $newid;
1364  } else {
1365  return false; // nothing changed
1366  }
1367  }
1368 
1384  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1385  $lastRevIsRedirect = null
1386  ) {
1387  global $wgContentHandlerUseDB;
1388 
1389  // TODO: move into PageUpdater or PageStore
1390  // NOTE: when doing that, make sure cached fields get reset in doEditContent,
1391  // and in the compat stub!
1392 
1393  // Assertion to try to catch T92046
1394  if ( (int)$revision->getId() === 0 ) {
1395  throw new InvalidArgumentException(
1396  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1397  );
1398  }
1399 
1400  $content = $revision->getContent();
1401  $len = $content ? $content->getSize() : 0;
1402  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1403 
1404  $conditions = [ 'page_id' => $this->getId() ];
1405 
1406  if ( !is_null( $lastRevision ) ) {
1407  // An extra check against threads stepping on each other
1408  $conditions['page_latest'] = $lastRevision;
1409  }
1410 
1411  $revId = $revision->getId();
1412  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1413 
1414  $row = [ /* SET */
1415  'page_latest' => $revId,
1416  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1417  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1418  'page_is_redirect' => $rt !== null ? 1 : 0,
1419  'page_len' => $len,
1420  ];
1421 
1422  if ( $wgContentHandlerUseDB ) {
1423  $row['page_content_model'] = $revision->getContentModel();
1424  }
1425 
1426  $dbw->update( 'page',
1427  $row,
1428  $conditions,
1429  __METHOD__ );
1430 
1431  $result = $dbw->affectedRows() > 0;
1432  if ( $result ) {
1433  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1434  $this->setLastEdit( $revision );
1435  $this->mLatest = $revision->getId();
1436  $this->mIsRedirect = (bool)$rt;
1437  // Update the LinkCache.
1438  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1439  $linkCache->addGoodLinkObj(
1440  $this->getId(),
1441  $this->mTitle,
1442  $len,
1443  $this->mIsRedirect,
1444  $this->mLatest,
1445  $revision->getContentModel()
1446  );
1447  }
1448 
1449  return $result;
1450  }
1451 
1463  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1464  // Always update redirects (target link might have changed)
1465  // Update/Insert if we don't know if the last revision was a redirect or not
1466  // Delete if changing from redirect to non-redirect
1467  $isRedirect = !is_null( $redirectTitle );
1468 
1469  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1470  return true;
1471  }
1472 
1473  if ( $isRedirect ) {
1474  $success = $this->insertRedirectEntry( $redirectTitle );
1475  } else {
1476  // This is not a redirect, remove row from redirect table
1477  $where = [ 'rd_from' => $this->getId() ];
1478  $dbw->delete( 'redirect', $where, __METHOD__ );
1479  $success = true;
1480  }
1481 
1482  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1483  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1484  }
1485 
1486  return $success;
1487  }
1488 
1499  public function updateIfNewerOn( $dbw, $revision ) {
1500  $row = $dbw->selectRow(
1501  [ 'revision', 'page' ],
1502  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1503  [
1504  'page_id' => $this->getId(),
1505  'page_latest=rev_id' ],
1506  __METHOD__ );
1507 
1508  if ( $row ) {
1509  if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1510  return false;
1511  }
1512  $prev = $row->rev_id;
1513  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1514  } else {
1515  // No or missing previous revision; mark the page as new
1516  $prev = 0;
1517  $lastRevIsRedirect = null;
1518  }
1519 
1520  $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1521 
1522  return $ret;
1523  }
1524 
1537  public static function hasDifferencesOutsideMainSlot( Revision $a, Revision $b ) {
1538  $aSlots = $a->getRevisionRecord()->getSlots();
1539  $bSlots = $b->getRevisionRecord()->getSlots();
1540  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1541 
1542  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1543  }
1544 
1556  public function getUndoContent( Revision $undo, Revision $undoafter ) {
1557  // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1558 
1559  if ( self::hasDifferencesOutsideMainSlot( $undo, $undoafter ) ) {
1560  // Cannot yet undo edits that involve anything other the main slot.
1561  return false;
1562  }
1563 
1564  $handler = $undo->getContentHandler();
1565  return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1566  }
1567 
1578  public function supportsSections() {
1579  return $this->getContentHandler()->supportsSections();
1580  }
1581 
1596  public function replaceSectionContent(
1597  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1598  ) {
1599  $baseRevId = null;
1600  if ( $edittime && $sectionId !== 'new' ) {
1601  $lb = $this->getDBLoadBalancer();
1602  $dbr = $lb->getConnectionRef( DB_REPLICA );
1603  $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1604  // Try the master if this thread may have just added it.
1605  // This could be abstracted into a Revision method, but we don't want
1606  // to encourage loading of revisions by timestamp.
1607  if ( !$rev
1608  && $lb->getServerCount() > 1
1609  && $lb->hasOrMadeRecentMasterChanges()
1610  ) {
1611  $dbw = $lb->getConnectionRef( DB_MASTER );
1612  $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1613  }
1614  if ( $rev ) {
1615  $baseRevId = $rev->getId();
1616  }
1617  }
1618 
1619  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1620  }
1621 
1635  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1636  $sectionTitle = '', $baseRevId = null
1637  ) {
1638  if ( strval( $sectionId ) === '' ) {
1639  // Whole-page edit; let the whole text through
1640  $newContent = $sectionContent;
1641  } else {
1642  if ( !$this->supportsSections() ) {
1643  throw new MWException( "sections not supported for content model " .
1644  $this->getContentHandler()->getModelID() );
1645  }
1646 
1647  // T32711: always use current version when adding a new section
1648  if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1649  $oldContent = $this->getContent();
1650  } else {
1651  $rev = Revision::newFromId( $baseRevId );
1652  if ( !$rev ) {
1653  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1654  $this->getId() . "; section: $sectionId)\n" );
1655  return null;
1656  }
1657 
1658  $oldContent = $rev->getContent();
1659  }
1660 
1661  if ( !$oldContent ) {
1662  wfDebug( __METHOD__ . ": no page text\n" );
1663  return null;
1664  }
1665 
1666  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1667  }
1668 
1669  return $newContent;
1670  }
1671 
1681  public function checkFlags( $flags ) {
1682  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1683  if ( $this->exists() ) {
1684  $flags |= EDIT_UPDATE;
1685  } else {
1686  $flags |= EDIT_NEW;
1687  }
1688  }
1689 
1690  return $flags;
1691  }
1692 
1696  private function newDerivedDataUpdater() {
1698 
1700  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1701  $this->getRevisionStore(),
1702  $this->getRevisionRenderer(),
1703  $this->getSlotRoleRegistry(),
1704  $this->getParserCache(),
1707  MediaWikiServices::getInstance()->getContentLanguage(),
1708  MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
1709  );
1710 
1711  $derivedDataUpdater->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
1712  $derivedDataUpdater->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership );
1713  $derivedDataUpdater->setArticleCountMethod( $wgArticleCountMethod );
1714 
1715  return $derivedDataUpdater;
1716  }
1717 
1745  private function getDerivedDataUpdater(
1746  User $forUser = null,
1747  RevisionRecord $forRevision = null,
1748  RevisionSlotsUpdate $forUpdate = null,
1749  $forEdit = false
1750  ) {
1751  if ( !$forRevision && !$forUpdate ) {
1752  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1753  // going to use it with.
1754  $this->derivedDataUpdater = null;
1755  }
1756 
1757  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1758  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1759  // to it did not yet initialize it, because we don't know what data it will be
1760  // initialized with.
1761  $this->derivedDataUpdater = null;
1762  }
1763 
1764  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1765  // However, there is no good way to construct a cache key. We'd need to check against all
1766  // cached instances.
1767 
1768  if ( $this->derivedDataUpdater
1769  && !$this->derivedDataUpdater->isReusableFor(
1770  $forUser,
1771  $forRevision,
1772  $forUpdate,
1773  $forEdit ? $this->getLatest() : null
1774  )
1775  ) {
1776  $this->derivedDataUpdater = null;
1777  }
1778 
1779  if ( !$this->derivedDataUpdater ) {
1780  $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1781  }
1782 
1784  }
1785 
1801  public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1803 
1804  $pageUpdater = new PageUpdater(
1805  $user,
1806  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1807  $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1808  $this->getDBLoadBalancer(),
1809  $this->getRevisionStore(),
1810  $this->getSlotRoleRegistry()
1811  );
1812 
1813  $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
1814  $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
1815  $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
1816 
1817  return $pageUpdater;
1818  }
1819 
1882  public function doEditContent(
1883  Content $content, $summary, $flags = 0, $originalRevId = false,
1884  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1885  ) {
1886  global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1887 
1888  if ( !( $summary instanceof CommentStoreComment ) ) {
1889  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1890  }
1891 
1892  if ( !$user ) {
1893  $user = $wgUser;
1894  }
1895 
1896  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1897  // Checking the minoredit right should be done in the same place the 'bot' right is
1898  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1899  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1900  if ( ( $flags & EDIT_MINOR ) && !$permissionManager->userHasRight( $user, 'minoredit' ) ) {
1901  $flags = ( $flags & ~EDIT_MINOR );
1902  }
1903 
1904  $slotsUpdate = new RevisionSlotsUpdate();
1905  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1906 
1907  // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1908  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1909  // used by this PageUpdater. However, there is no guarantee for this.
1910  $updater = $this->newPageUpdater( $user, $slotsUpdate );
1911  $updater->setContent( SlotRecord::MAIN, $content );
1912  $updater->setOriginalRevisionId( $originalRevId );
1913  $updater->setUndidRevisionId( $undidRevId );
1914 
1915  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1916 
1917  // TODO: this logic should not be in the storage layer, it's here for compatibility
1918  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1919  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1920 
1921  if ( $needsPatrol && $permissionManager->userCan(
1922  'autopatrol', $user, $this->getTitle()
1923  ) ) {
1924  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1925  }
1926 
1927  $updater->addTags( $tags );
1928 
1929  $revRec = $updater->saveRevision(
1930  $summary,
1931  $flags
1932  );
1933 
1934  // $revRec will be null if the edit failed, or if no new revision was created because
1935  // the content did not change.
1936  if ( $revRec ) {
1937  // update cached fields
1938  // TODO: this is currently redundant to what is done in updateRevisionOn.
1939  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1940  $this->setLastEdit( new Revision( $revRec ) ); // TODO: use RevisionRecord
1941  $this->mLatest = $revRec->getId();
1942  }
1943 
1944  return $updater->getStatus();
1945  }
1946 
1961  public function makeParserOptions( $context ) {
1962  $options = ParserOptions::newCanonical( $context );
1963 
1964  if ( $this->getTitle()->isConversionTable() ) {
1965  // @todo ConversionTable should become a separate content model, so
1966  // we don't need special cases like this one.
1967  $options->disableContentConversion();
1968  }
1969 
1970  return $options;
1971  }
1972 
1991  public function prepareContentForEdit(
1992  Content $content,
1993  $revision = null,
1994  User $user = null,
1995  $serialFormat = null,
1996  $useCache = true
1997  ) {
1998  global $wgUser;
1999 
2000  if ( !$user ) {
2001  $user = $wgUser;
2002  }
2003 
2004  if ( $revision !== null ) {
2005  if ( $revision instanceof Revision ) {
2006  $revision = $revision->getRevisionRecord();
2007  } elseif ( !( $revision instanceof RevisionRecord ) ) {
2008  throw new InvalidArgumentException(
2009  __METHOD__ . ': invalid $revision argument type ' . gettype( $revision ) );
2010  }
2011  }
2012 
2013  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2014  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2015 
2016  if ( !$updater->isUpdatePrepared() ) {
2017  $updater->prepareContent( $user, $slots, $useCache );
2018 
2019  if ( $revision ) {
2020  $updater->prepareUpdate(
2021  $revision,
2022  [
2023  'causeAction' => 'prepare-edit',
2024  'causeAgent' => $user->getName(),
2025  ]
2026  );
2027  }
2028  }
2029 
2030  return $updater->getPreparedEdit();
2031  }
2032 
2060  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2061  $options += [
2062  'causeAction' => 'edit-page',
2063  'causeAgent' => $user->getName(),
2064  ];
2065 
2066  $revision = $revision->getRevisionRecord();
2067 
2068  $updater = $this->getDerivedDataUpdater( $user, $revision );
2069 
2070  $updater->prepareUpdate( $revision, $options );
2071 
2072  $updater->doUpdates();
2073  }
2074 
2088  public function updateParserCache( array $options = [] ) {
2089  $revision = $this->getRevisionRecord();
2090  if ( !$revision || !$revision->getId() ) {
2091  LoggerFactory::getInstance( 'wikipage' )->info(
2092  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2093  );
2094  return;
2095  }
2096  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2097 
2098  $updater = $this->getDerivedDataUpdater( $user, $revision );
2099  $updater->prepareUpdate( $revision, $options );
2100  $updater->doParserCacheUpdate();
2101  }
2102 
2132  public function doSecondaryDataUpdates( array $options = [] ) {
2133  $options['recursive'] = $options['recursive'] ?? true;
2134  $revision = $this->getRevisionRecord();
2135  if ( !$revision || !$revision->getId() ) {
2136  LoggerFactory::getInstance( 'wikipage' )->info(
2137  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2138  );
2139  return;
2140  }
2141  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2142 
2143  $updater = $this->getDerivedDataUpdater( $user, $revision );
2144  $updater->prepareUpdate( $revision, $options );
2145  $updater->doSecondaryDataUpdates( $options );
2146  }
2147 
2162  public function doUpdateRestrictions( array $limit, array $expiry,
2163  &$cascade, $reason, User $user, $tags = null
2164  ) {
2166 
2167  if ( wfReadOnly() ) {
2168  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2169  }
2170 
2171  $this->loadPageData( 'fromdbmaster' );
2172  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2173  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2174  $id = $this->getId();
2175 
2176  if ( !$cascade ) {
2177  $cascade = false;
2178  }
2179 
2180  // Take this opportunity to purge out expired restrictions
2182 
2183  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2184  // we expect a single selection, but the schema allows otherwise.
2185  $isProtected = false;
2186  $protect = false;
2187  $changed = false;
2188 
2189  $dbw = wfGetDB( DB_MASTER );
2190 
2191  foreach ( $restrictionTypes as $action ) {
2192  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2193  $expiry[$action] = 'infinity';
2194  }
2195  if ( !isset( $limit[$action] ) ) {
2196  $limit[$action] = '';
2197  } elseif ( $limit[$action] != '' ) {
2198  $protect = true;
2199  }
2200 
2201  // Get current restrictions on $action
2202  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2203  if ( $current != '' ) {
2204  $isProtected = true;
2205  }
2206 
2207  if ( $limit[$action] != $current ) {
2208  $changed = true;
2209  } elseif ( $limit[$action] != '' ) {
2210  // Only check expiry change if the action is actually being
2211  // protected, since expiry does nothing on an not-protected
2212  // action.
2213  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2214  $changed = true;
2215  }
2216  }
2217  }
2218 
2219  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2220  $changed = true;
2221  }
2222 
2223  // If nothing has changed, do nothing
2224  if ( !$changed ) {
2225  return Status::newGood();
2226  }
2227 
2228  if ( !$protect ) { // No protection at all means unprotection
2229  $revCommentMsg = 'unprotectedarticle-comment';
2230  $logAction = 'unprotect';
2231  } elseif ( $isProtected ) {
2232  $revCommentMsg = 'modifiedarticleprotection-comment';
2233  $logAction = 'modify';
2234  } else {
2235  $revCommentMsg = 'protectedarticle-comment';
2236  $logAction = 'protect';
2237  }
2238 
2239  $logRelationsValues = [];
2240  $logRelationsField = null;
2241  $logParamsDetails = [];
2242 
2243  // Null revision (used for change tag insertion)
2244  $nullRevision = null;
2245 
2246  if ( $id ) { // Protection of existing page
2247  // Avoid PHP 7.1 warning of passing $this by reference
2248  $wikiPage = $this;
2249 
2250  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2251  return Status::newGood();
2252  }
2253 
2254  // Only certain restrictions can cascade...
2255  $editrestriction = isset( $limit['edit'] )
2256  ? [ $limit['edit'] ]
2257  : $this->mTitle->getRestrictions( 'edit' );
2258  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2259  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2260  }
2261  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2262  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2263  }
2264 
2265  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2266  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2267  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2268  }
2269  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2270  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2271  }
2272 
2273  // The schema allows multiple restrictions
2274  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2275  $cascade = false;
2276  }
2277 
2278  // insert null revision to identify the page protection change as edit summary
2279  $latest = $this->getLatest();
2280  $nullRevision = $this->insertProtectNullRevision(
2281  $revCommentMsg,
2282  $limit,
2283  $expiry,
2284  $cascade,
2285  $reason,
2286  $user
2287  );
2288 
2289  if ( $nullRevision === null ) {
2290  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2291  }
2292 
2293  $logRelationsField = 'pr_id';
2294 
2295  // T214035: Avoid deadlock on MySQL.
2296  // Do a DELETE by primary key (pr_id) for any existing protection rows.
2297  // On MySQL and derivatives, unconditionally deleting by page ID (pr_page) would.
2298  // place a gap lock if there are no matching rows. This can deadlock when another
2299  // thread modifies protection settings for page IDs in the same gap.
2300  $existingProtectionIds = $dbw->selectFieldValues(
2301  'page_restrictions',
2302  'pr_id',
2303  [
2304  'pr_page' => $id,
2305  'pr_type' => array_keys( $limit )
2306  ],
2307  __METHOD__
2308  );
2309 
2310  if ( $existingProtectionIds ) {
2311  $dbw->delete(
2312  'page_restrictions',
2313  [ 'pr_id' => $existingProtectionIds ],
2314  __METHOD__
2315  );
2316  }
2317 
2318  // Update restrictions table
2319  foreach ( $limit as $action => $restrictions ) {
2320  if ( $restrictions != '' ) {
2321  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2322  $dbw->insert(
2323  'page_restrictions',
2324  [
2325  'pr_page' => $id,
2326  'pr_type' => $action,
2327  'pr_level' => $restrictions,
2328  'pr_cascade' => $cascadeValue,
2329  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2330  ],
2331  __METHOD__
2332  );
2333  $logRelationsValues[] = $dbw->insertId();
2334  $logParamsDetails[] = [
2335  'type' => $action,
2336  'level' => $restrictions,
2337  'expiry' => $expiry[$action],
2338  'cascade' => (bool)$cascadeValue,
2339  ];
2340  }
2341  }
2342 
2343  // Clear out legacy restriction fields
2344  $dbw->update(
2345  'page',
2346  [ 'page_restrictions' => '' ],
2347  [ 'page_id' => $id ],
2348  __METHOD__
2349  );
2350 
2351  // Avoid PHP 7.1 warning of passing $this by reference
2352  $wikiPage = $this;
2353 
2354  Hooks::run( 'NewRevisionFromEditComplete',
2355  [ $this, $nullRevision, $latest, $user ] );
2356  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2357  } else { // Protection of non-existing page (also known as "title protection")
2358  // Cascade protection is meaningless in this case
2359  $cascade = false;
2360 
2361  if ( $limit['create'] != '' ) {
2362  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2363  $dbw->replace( 'protected_titles',
2364  [ [ 'pt_namespace', 'pt_title' ] ],
2365  [
2366  'pt_namespace' => $this->mTitle->getNamespace(),
2367  'pt_title' => $this->mTitle->getDBkey(),
2368  'pt_create_perm' => $limit['create'],
2369  'pt_timestamp' => $dbw->timestamp(),
2370  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2371  'pt_user' => $user->getId(),
2372  ] + $commentFields, __METHOD__
2373  );
2374  $logParamsDetails[] = [
2375  'type' => 'create',
2376  'level' => $limit['create'],
2377  'expiry' => $expiry['create'],
2378  ];
2379  } else {
2380  $dbw->delete( 'protected_titles',
2381  [
2382  'pt_namespace' => $this->mTitle->getNamespace(),
2383  'pt_title' => $this->mTitle->getDBkey()
2384  ], __METHOD__
2385  );
2386  }
2387  }
2388 
2389  $this->mTitle->flushRestrictions();
2390  InfoAction::invalidateCache( $this->mTitle );
2391 
2392  if ( $logAction == 'unprotect' ) {
2393  $params = [];
2394  } else {
2395  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2396  $params = [
2397  '4::description' => $protectDescriptionLog, // parameter for IRC
2398  '5:bool:cascade' => $cascade,
2399  'details' => $logParamsDetails, // parameter for localize and api
2400  ];
2401  }
2402 
2403  // Update the protection log
2404  $logEntry = new ManualLogEntry( 'protect', $logAction );
2405  $logEntry->setTarget( $this->mTitle );
2406  $logEntry->setComment( $reason );
2407  $logEntry->setPerformer( $user );
2408  $logEntry->setParameters( $params );
2409  if ( !is_null( $nullRevision ) ) {
2410  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2411  }
2412  $logEntry->addTags( $tags );
2413  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2414  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2415  }
2416  $logId = $logEntry->insert();
2417  $logEntry->publish( $logId );
2418 
2419  return Status::newGood( $logId );
2420  }
2421 
2433  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2434  array $expiry, $cascade, $reason, $user = null
2435  ) {
2436  $dbw = wfGetDB( DB_MASTER );
2437 
2438  // Prepare a null revision to be added to the history
2439  $editComment = wfMessage(
2440  $revCommentMsg,
2441  $this->mTitle->getPrefixedText(),
2442  $user ? $user->getName() : ''
2443  )->inContentLanguage()->text();
2444  if ( $reason ) {
2445  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2446  }
2447  $protectDescription = $this->protectDescription( $limit, $expiry );
2448  if ( $protectDescription ) {
2449  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2450  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2451  ->inContentLanguage()->text();
2452  }
2453  if ( $cascade ) {
2454  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2455  $editComment .= wfMessage( 'brackets' )->params(
2456  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2457  )->inContentLanguage()->text();
2458  }
2459 
2460  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2461  if ( $nullRev ) {
2462  $nullRev->insertOn( $dbw );
2463 
2464  // Update page record and touch page
2465  $oldLatest = $nullRev->getParentId();
2466  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2467  }
2468 
2469  return $nullRev;
2470  }
2471 
2476  protected function formatExpiry( $expiry ) {
2477  if ( $expiry != 'infinity' ) {
2478  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2479  return wfMessage(
2480  'protect-expiring',
2481  $contLang->timeanddate( $expiry, false, false ),
2482  $contLang->date( $expiry, false, false ),
2483  $contLang->time( $expiry, false, false )
2484  )->inContentLanguage()->text();
2485  } else {
2486  return wfMessage( 'protect-expiry-indefinite' )
2487  ->inContentLanguage()->text();
2488  }
2489  }
2490 
2498  public function protectDescription( array $limit, array $expiry ) {
2499  $protectDescription = '';
2500 
2501  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2502  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2503  # All possible message keys are listed here for easier grepping:
2504  # * restriction-create
2505  # * restriction-edit
2506  # * restriction-move
2507  # * restriction-upload
2508  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2509  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2510  # with '' filtered out. All possible message keys are listed below:
2511  # * protect-level-autoconfirmed
2512  # * protect-level-sysop
2513  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2514  ->inContentLanguage()->text();
2515 
2516  $expiryText = $this->formatExpiry( $expiry[$action] );
2517 
2518  if ( $protectDescription !== '' ) {
2519  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2520  }
2521  $protectDescription .= wfMessage( 'protect-summary-desc' )
2522  ->params( $actionText, $restrictionsText, $expiryText )
2523  ->inContentLanguage()->text();
2524  }
2525 
2526  return $protectDescription;
2527  }
2528 
2540  public function protectDescriptionLog( array $limit, array $expiry ) {
2541  $protectDescriptionLog = '';
2542 
2543  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2544  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2545  $expiryText = $this->formatExpiry( $expiry[$action] );
2546  $protectDescriptionLog .=
2547  $dirMark .
2548  "[$action=$restrictions] ($expiryText)";
2549  }
2550 
2551  return trim( $protectDescriptionLog );
2552  }
2553 
2566  public function isBatchedDelete( $safetyMargin = 0 ) {
2568 
2569  $dbr = wfGetDB( DB_REPLICA );
2570  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2571  $revCount += $safetyMargin;
2572 
2573  return $revCount >= $wgDeleteRevisionsBatchSize;
2574  }
2575 
2595  public function doDeleteArticle(
2596  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2597  $immediate = false
2598  ) {
2599  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2600  [], 'delete', $immediate );
2601 
2602  // Returns true if the page was actually deleted, or is scheduled for deletion
2603  return $status->isOK();
2604  }
2605 
2628  public function doDeleteArticleReal(
2629  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2630  $tags = [], $logsubtype = 'delete', $immediate = false
2631  ) {
2632  global $wgUser;
2633 
2634  wfDebug( __METHOD__ . "\n" );
2635 
2636  $status = Status::newGood();
2637 
2638  // Avoid PHP 7.1 warning of passing $this by reference
2639  $wikiPage = $this;
2640 
2641  if ( !$deleter ) {
2642  $deleter = $wgUser;
2643  }
2644  if ( !Hooks::run( 'ArticleDelete',
2645  [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2646  ) ) {
2647  if ( $status->isOK() ) {
2648  // Hook aborted but didn't set a fatal status
2649  $status->fatal( 'delete-hook-aborted' );
2650  }
2651  return $status;
2652  }
2653 
2654  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2655  $logsubtype, $immediate );
2656  }
2657 
2674  public function doDeleteArticleBatched(
2675  $reason, $suppress, User $deleter, $tags,
2676  $logsubtype, $immediate = false, $webRequestId = null
2677  ) {
2678  wfDebug( __METHOD__ . "\n" );
2679 
2680  $status = Status::newGood();
2681 
2682  $dbw = wfGetDB( DB_MASTER );
2683  $dbw->startAtomic( __METHOD__ );
2684 
2685  $this->loadPageData( self::READ_LATEST );
2686  $id = $this->getId();
2687  // T98706: lock the page from various other updates but avoid using
2688  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2689  // the revisions queries (which also JOIN on user). Only lock the page
2690  // row and CAS check on page_latest to see if the trx snapshot matches.
2691  $lockedLatest = $this->lockAndGetLatest();
2692  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2693  $dbw->endAtomic( __METHOD__ );
2694  // Page not there or trx snapshot is stale
2695  $status->error( 'cannotdelete',
2696  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2697  return $status;
2698  }
2699 
2700  // At this point we are now committed to returning an OK
2701  // status unless some DB query error or other exception comes up.
2702  // This way callers don't have to call rollback() if $status is bad
2703  // unless they actually try to catch exceptions (which is rare).
2704 
2705  // we need to remember the old content so we can use it to generate all deletion updates.
2706  $revision = $this->getRevision();
2707  try {
2708  $content = $this->getContent( RevisionRecord::RAW );
2709  } catch ( Exception $ex ) {
2710  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2711  . $ex->getMessage() );
2712 
2713  $content = null;
2714  }
2715 
2716  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2717  // one batch of revisions and defer archival of any others to the job queue.
2718  $explictTrxLogged = false;
2719  while ( true ) {
2720  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2721  if ( $done || !$immediate ) {
2722  break;
2723  }
2724  $dbw->endAtomic( __METHOD__ );
2725  if ( $dbw->explicitTrxActive() ) {
2726  // Explict transactions may never happen here in practice. Log to be sure.
2727  if ( !$explictTrxLogged ) {
2728  $explictTrxLogged = true;
2729  LoggerFactory::getInstance( 'wfDebug' )->debug(
2730  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2731  'title' => $this->getTitle()->getText(),
2732  ] );
2733  }
2734  continue;
2735  }
2736  if ( $dbw->trxLevel() ) {
2737  $dbw->commit();
2738  }
2739  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2740  $lbFactory->waitForReplication();
2741  $dbw->startAtomic( __METHOD__ );
2742  }
2743 
2744  // If done archiving, also delete the article.
2745  if ( !$done ) {
2746  $dbw->endAtomic( __METHOD__ );
2747 
2748  $jobParams = [
2749  'namespace' => $this->getTitle()->getNamespace(),
2750  'title' => $this->getTitle()->getDBkey(),
2751  'wikiPageId' => $id,
2752  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2753  'reason' => $reason,
2754  'suppress' => $suppress,
2755  'userId' => $deleter->getId(),
2756  'tags' => json_encode( $tags ),
2757  'logsubtype' => $logsubtype,
2758  ];
2759 
2760  $job = new DeletePageJob( $jobParams );
2761  JobQueueGroup::singleton()->push( $job );
2762 
2763  $status->warning( 'delete-scheduled',
2764  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2765  } else {
2766  // Get archivedRevisionCount by db query, because there's no better alternative.
2767  // Jobs cannot pass a count of archived revisions to the next job, because additional
2768  // deletion operations can be started while the first is running. Jobs from each
2769  // gracefully interleave, but would not know about each other's count. Deduplication
2770  // in the job queue to avoid simultaneous deletion operations would add overhead.
2771  // Number of archived revisions cannot be known beforehand, because edits can be made
2772  // while deletion operations are being processed, changing the number of archivals.
2773  $archivedRevisionCount = (int)$dbw->selectField(
2774  'archive', 'COUNT(*)',
2775  [
2776  'ar_namespace' => $this->getTitle()->getNamespace(),
2777  'ar_title' => $this->getTitle()->getDBkey(),
2778  'ar_page_id' => $id
2779  ], __METHOD__
2780  );
2781 
2782  // Clone the title and wikiPage, so we have the information we need when
2783  // we log and run the ArticleDeleteComplete hook.
2784  $logTitle = clone $this->mTitle;
2785  $wikiPageBeforeDelete = clone $this;
2786 
2787  // Now that it's safely backed up, delete it
2788  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2789 
2790  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2791  $logtype = $suppress ? 'suppress' : 'delete';
2792 
2793  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2794  $logEntry->setPerformer( $deleter );
2795  $logEntry->setTarget( $logTitle );
2796  $logEntry->setComment( $reason );
2797  $logEntry->addTags( $tags );
2798  $logid = $logEntry->insert();
2799 
2800  $dbw->onTransactionPreCommitOrIdle(
2801  function () use ( $logEntry, $logid ) {
2802  // T58776: avoid deadlocks (especially from FileDeleteForm)
2803  $logEntry->publish( $logid );
2804  },
2805  __METHOD__
2806  );
2807 
2808  $dbw->endAtomic( __METHOD__ );
2809 
2810  $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2811 
2812  Hooks::run( 'ArticleDeleteComplete', [
2813  &$wikiPageBeforeDelete,
2814  &$deleter,
2815  $reason,
2816  $id,
2817  $content,
2818  $logEntry,
2819  $archivedRevisionCount
2820  ] );
2821  $status->value = $logid;
2822 
2823  // Show log excerpt on 404 pages rather than just a link
2824  $dbCache = ObjectCache::getInstance( 'db-replicated' );
2825  $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2826  $dbCache->set( $key, 1, $dbCache::TTL_DAY );
2827  }
2828 
2829  return $status;
2830  }
2831 
2841  protected function archiveRevisions( $dbw, $id, $suppress ) {
2844 
2845  // Given the lock above, we can be confident in the title and page ID values
2846  $namespace = $this->getTitle()->getNamespace();
2847  $dbKey = $this->getTitle()->getDBkey();
2848 
2849  $commentStore = CommentStore::getStore();
2850  $actorMigration = ActorMigration::newMigration();
2851 
2853  $bitfield = false;
2854 
2855  // Bitfields to further suppress the content
2856  if ( $suppress ) {
2857  $bitfield = RevisionRecord::SUPPRESSED_ALL;
2858  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2859  }
2860 
2861  // For now, shunt the revision data into the archive table.
2862  // Text is *not* removed from the text table; bulk storage
2863  // is left intact to avoid breaking block-compression or
2864  // immutable storage schemes.
2865  // In the future, we may keep revisions and mark them with
2866  // the rev_deleted field, which is reserved for this purpose.
2867 
2868  // Lock rows in `revision` and its temp tables, but not any others.
2869  // Note array_intersect() preserves keys from the first arg, and we're
2870  // assuming $revQuery has `revision` primary and isn't using subtables
2871  // for anything we care about.
2872  $dbw->lockForUpdate(
2873  array_intersect(
2874  $revQuery['tables'],
2875  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2876  ),
2877  [ 'rev_page' => $id ],
2878  __METHOD__,
2879  [],
2880  $revQuery['joins']
2881  );
2882 
2883  // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
2884  // so we can copy it to the archive table.
2885  // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
2886  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2887  $revQuery['fields'][] = 'rev_text_id';
2888 
2889  if ( $wgContentHandlerUseDB ) {
2890  $revQuery['fields'][] = 'rev_content_model';
2891  $revQuery['fields'][] = 'rev_content_format';
2892  }
2893  }
2894 
2895  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2896  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2897  $res = $dbw->select(
2898  $revQuery['tables'],
2899  $revQuery['fields'],
2900  [ 'rev_page' => $id ],
2901  __METHOD__,
2902  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2903  $revQuery['joins']
2904  );
2905 
2906  // Build their equivalent archive rows
2907  $rowsInsert = [];
2908  $revids = [];
2909 
2911  $ipRevIds = [];
2912 
2913  $done = true;
2914  foreach ( $res as $row ) {
2915  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2916  $done = false;
2917  break;
2918  }
2919 
2920  $comment = $commentStore->getComment( 'rev_comment', $row );
2921  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2922  $rowInsert = [
2923  'ar_namespace' => $namespace,
2924  'ar_title' => $dbKey,
2925  'ar_timestamp' => $row->rev_timestamp,
2926  'ar_minor_edit' => $row->rev_minor_edit,
2927  'ar_rev_id' => $row->rev_id,
2928  'ar_parent_id' => $row->rev_parent_id,
2937  'ar_len' => $row->rev_len,
2938  'ar_page_id' => $id,
2939  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2940  'ar_sha1' => $row->rev_sha1,
2941  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2942  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2943 
2944  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2945  $rowInsert['ar_text_id'] = $row->rev_text_id;
2946 
2947  if ( $wgContentHandlerUseDB ) {
2948  $rowInsert['ar_content_model'] = $row->rev_content_model;
2949  $rowInsert['ar_content_format'] = $row->rev_content_format;
2950  }
2951  }
2952 
2953  $rowsInsert[] = $rowInsert;
2954  $revids[] = $row->rev_id;
2955 
2956  // Keep track of IP edits, so that the corresponding rows can
2957  // be deleted in the ip_changes table.
2958  if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2959  $ipRevIds[] = $row->rev_id;
2960  }
2961  }
2962 
2963  // This conditional is just a sanity check
2964  if ( count( $revids ) > 0 ) {
2965  // Copy them into the archive table
2966  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2967 
2968  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2969  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2970  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2971 
2972  // Also delete records from ip_changes as applicable.
2973  if ( count( $ipRevIds ) > 0 ) {
2974  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2975  }
2976  }
2977 
2978  return $done;
2979  }
2980 
2987  public function lockAndGetLatest() {
2988  return (int)wfGetDB( DB_MASTER )->selectField(
2989  'page',
2990  'page_latest',
2991  [
2992  'page_id' => $this->getId(),
2993  // Typically page_id is enough, but some code might try to do
2994  // updates assuming the title is the same, so verify that
2995  'page_namespace' => $this->getTitle()->getNamespace(),
2996  'page_title' => $this->getTitle()->getDBkey()
2997  ],
2998  __METHOD__,
2999  [ 'FOR UPDATE' ]
3000  );
3001  }
3002 
3015  public function doDeleteUpdates(
3016  $id, Content $content = null, Revision $revision = null, User $user = null
3017  ) {
3018  if ( $id !== $this->getId() ) {
3019  throw new InvalidArgumentException( 'Mismatching page ID' );
3020  }
3021 
3022  try {
3023  $countable = $this->isCountable();
3024  } catch ( Exception $ex ) {
3025  // fallback for deleting broken pages for which we cannot load the content for
3026  // some reason. Note that doDeleteArticleReal() already logged this problem.
3027  $countable = false;
3028  }
3029 
3030  // Update site status
3032  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3033  ) );
3034 
3035  // Delete pagelinks, update secondary indexes, etc
3036  $updates = $this->getDeletionUpdates(
3037  $revision ? $revision->getRevisionRecord() : $content
3038  );
3039  foreach ( $updates as $update ) {
3040  DeferredUpdates::addUpdate( $update );
3041  }
3042 
3043  $causeAgent = $user ? $user->getName() : 'unknown';
3044  // Reparse any pages transcluding this page
3046  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3047  // Reparse any pages including this image
3048  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3050  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3051  }
3052 
3053  // Clear caches
3054  self::onArticleDelete( $this->mTitle );
3056  $this->mTitle,
3057  $revision,
3058  null,
3060  );
3061 
3062  // Reset this object and the Title object
3063  $this->loadFromRow( false, self::READ_LATEST );
3064 
3065  // Search engine
3066  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3067  }
3068 
3098  public function doRollback(
3099  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3100  ) {
3101  $resultDetails = null;
3102 
3103  // Check permissions
3104  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3105  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3106  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3107 
3108  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3109  $errors[] = [ 'sessionfailure' ];
3110  }
3111 
3112  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3113  $errors[] = [ 'actionthrottledtext' ];
3114  }
3115 
3116  // If there were errors, bail out now
3117  if ( !empty( $errors ) ) {
3118  return $errors;
3119  }
3120 
3121  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3122  }
3123 
3144  public function commitRollback( $fromP, $summary, $bot,
3145  &$resultDetails, User $guser, $tags = null
3146  ) {
3148 
3149  $dbw = wfGetDB( DB_MASTER );
3150 
3151  if ( wfReadOnly() ) {
3152  return [ [ 'readonlytext' ] ];
3153  }
3154 
3155  // Begin revision creation cycle by creating a PageUpdater.
3156  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3157  $updater = $this->newPageUpdater( $guser );
3158  $current = $updater->grabParentRevision();
3159 
3160  if ( is_null( $current ) ) {
3161  // Something wrong... no page?
3162  return [ [ 'notanarticle' ] ];
3163  }
3164 
3165  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3166  $legacyCurrent = new Revision( $current );
3167  $from = str_replace( '_', ' ', $fromP );
3168 
3169  // User name given should match up with the top revision.
3170  // If the revision's user is not visible, then $from should be empty.
3171  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3172  $resultDetails = [ 'current' => $legacyCurrent ];
3173  return [ [ 'alreadyrolled',
3174  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3175  htmlspecialchars( $fromP ),
3176  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3177  ] ];
3178  }
3179 
3180  // Get the last edit not by this person...
3181  // Note: these may not be public values
3182  $actorWhere = ActorMigration::newMigration()->getWhere(
3183  $dbw,
3184  'rev_user',
3185  $current->getUser( RevisionRecord::RAW )
3186  );
3187 
3188  $s = $dbw->selectRow(
3189  [ 'revision' ] + $actorWhere['tables'],
3190  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3191  [
3192  'rev_page' => $current->getPageId(),
3193  'NOT(' . $actorWhere['conds'] . ')',
3194  ],
3195  __METHOD__,
3196  [
3197  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3198  'ORDER BY' => 'rev_timestamp DESC'
3199  ],
3200  $actorWhere['joins']
3201  );
3202  if ( $s === false ) {
3203  // No one else ever edited this page
3204  return [ [ 'cantrollback' ] ];
3205  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3206  || $s->rev_deleted & RevisionRecord::DELETED_USER
3207  ) {
3208  // Only admins can see this text
3209  return [ [ 'notvisiblerev' ] ];
3210  }
3211 
3212  // Generate the edit summary if necessary
3213  $target = $this->getRevisionStore()->getRevisionById(
3214  $s->rev_id,
3215  RevisionStore::READ_LATEST
3216  );
3217  if ( empty( $summary ) ) {
3218  if ( !$currentEditorForPublic ) { // no public user name
3219  $summary = wfMessage( 'revertpage-nouser' );
3220  } elseif ( $wgDisableAnonTalk && $current->getUser() === 0 ) {
3221  $summary = wfMessage( 'revertpage-anon' );
3222  } else {
3223  $summary = wfMessage( 'revertpage' );
3224  }
3225  }
3226  $legacyTarget = new Revision( $target );
3227  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3228 
3229  // Allow the custom summary to use the same args as the default message
3230  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3231  $args = [
3232  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3233  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3234  $s->rev_id,
3235  $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3236  $current->getId(),
3237  $contLang->timeanddate( $current->getTimestamp() )
3238  ];
3239  if ( $summary instanceof Message ) {
3240  $summary = $summary->params( $args )->inContentLanguage()->text();
3241  } else {
3242  $summary = wfMsgReplaceArgs( $summary, $args );
3243  }
3244 
3245  // Trim spaces on user supplied text
3246  $summary = trim( $summary );
3247 
3248  // Save
3249  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3250 
3251  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
3252  if ( $permissionManager->userHasRight( $guser, 'minoredit' ) ) {
3253  $flags |= EDIT_MINOR;
3254  }
3255 
3256  if ( $bot && ( $permissionManager->userHasAnyRight( $guser, 'markbotedits', 'bot' ) ) ) {
3257  $flags |= EDIT_FORCE_BOT;
3258  }
3259 
3260  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3261  $currentContent = $current->getContent( SlotRecord::MAIN );
3262  $targetContent = $target->getContent( SlotRecord::MAIN );
3263  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3264 
3265  if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3266  $tags[] = 'mw-rollback';
3267  }
3268 
3269  // Build rollback revision:
3270  // Restore old content
3271  // TODO: MCR: test this once we can store multiple slots
3272  foreach ( $target->getSlots()->getSlots() as $slot ) {
3273  $updater->inheritSlot( $slot );
3274  }
3275 
3276  // Remove extra slots
3277  // TODO: MCR: test this once we can store multiple slots
3278  foreach ( $current->getSlotRoles() as $role ) {
3279  if ( !$target->hasSlot( $role ) ) {
3280  $updater->removeSlot( $role );
3281  }
3282  }
3283 
3284  $updater->setOriginalRevisionId( $target->getId() );
3285  // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3286  $updater->addTags( $tags );
3287 
3288  // TODO: this logic should not be in the storage layer, it's here for compatibility
3289  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3290  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3291 
3292  if ( $wgUseRCPatrol && $permissionManager->userCan(
3293  'autopatrol', $guser, $this->getTitle()
3294  ) ) {
3295  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3296  }
3297 
3298  // Actually store the rollback
3299  $rev = $updater->saveRevision(
3301  $flags
3302  );
3303 
3304  // Set patrolling and bot flag on the edits, which gets rollbacked.
3305  // This is done even on edit failure to have patrolling in that case (T64157).
3306  $set = [];
3307  if ( $bot && $permissionManager->userHasRight( $guser, 'markbotedits' ) ) {
3308  // Mark all reverted edits as bot
3309  $set['rc_bot'] = 1;
3310  }
3311 
3312  if ( $wgUseRCPatrol ) {
3313  // Mark all reverted edits as patrolled
3314  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3315  }
3316 
3317  if ( count( $set ) ) {
3318  $actorWhere = ActorMigration::newMigration()->getWhere(
3319  $dbw,
3320  'rc_user',
3321  $current->getUser( RevisionRecord::RAW ),
3322  false
3323  );
3324  $dbw->update( 'recentchanges', $set,
3325  [ /* WHERE */
3326  'rc_cur_id' => $current->getPageId(),
3327  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3328  $actorWhere['conds'], // No tables/joins are needed for rc_user
3329  ],
3330  __METHOD__
3331  );
3332  }
3333 
3334  if ( !$updater->wasSuccessful() ) {
3335  return $updater->getStatus()->getErrorsArray();
3336  }
3337 
3338  // Report if the edit was not created because it did not change the content.
3339  if ( $updater->isUnchanged() ) {
3340  $resultDetails = [ 'current' => $legacyCurrent ];
3341  return [ [ 'alreadyrolled',
3342  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3343  htmlspecialchars( $fromP ),
3344  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3345  ] ];
3346  }
3347 
3348  if ( $changingContentModel ) {
3349  // If the content model changed during the rollback,
3350  // make sure it gets logged to Special:Log/contentmodel
3351  $log = new ManualLogEntry( 'contentmodel', 'change' );
3352  $log->setPerformer( $guser );
3353  $log->setTarget( $this->mTitle );
3354  $log->setComment( $summary );
3355  $log->setParameters( [
3356  '4::oldmodel' => $currentContent->getModel(),
3357  '5::newmodel' => $targetContent->getModel(),
3358  ] );
3359 
3360  $logId = $log->insert( $dbw );
3361  $log->publish( $logId );
3362  }
3363 
3364  $revId = $rev->getId();
3365 
3366  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3367 
3368  $resultDetails = [
3369  'summary' => $summary,
3370  'current' => $legacyCurrent,
3371  'target' => $legacyTarget,
3372  'newid' => $revId,
3373  'tags' => $tags
3374  ];
3375 
3376  // TODO: make this return a Status object and wrap $resultDetails in that.
3377  return [];
3378  }
3379 
3391  public static function onArticleCreate( Title $title ) {
3392  // TODO: move this into a PageEventEmitter service
3393 
3394  // Update existence markers on article/talk tabs...
3395  $other = $title->getOtherPage();
3396 
3397  $other->purgeSquid();
3398 
3399  $title->touchLinks();
3400  $title->purgeSquid();
3401  $title->deleteTitleProtection();
3402 
3403  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3404 
3405  // Invalidate caches of articles which include this page
3407  $title,
3408  'templatelinks',
3409  [ 'causeAction' => 'page-create' ]
3410  );
3411  JobQueueGroup::singleton()->lazyPush( $job );
3412 
3413  if ( $title->getNamespace() == NS_CATEGORY ) {
3414  // Load the Category object, which will schedule a job to create
3415  // the category table row if necessary. Checking a replica DB is ok
3416  // here, in the worst case it'll run an unnecessary recount job on
3417  // a category that probably doesn't have many members.
3418  Category::newFromTitle( $title )->getID();
3419  }
3420  }
3421 
3427  public static function onArticleDelete( Title $title ) {
3428  // TODO: move this into a PageEventEmitter service
3429 
3430  // Update existence markers on article/talk tabs...
3431  // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3432  BacklinkCache::get( $title )->clear();
3433  $other = $title->getOtherPage();
3434 
3435  $other->purgeSquid();
3436 
3437  $title->touchLinks();
3438  $title->purgeSquid();
3439 
3440  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3441 
3442  // File cache
3444  InfoAction::invalidateCache( $title );
3445 
3446  // Messages
3447  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3448  MessageCache::singleton()->updateMessageOverride( $title, null );
3449  }
3450 
3451  // Images
3452  if ( $title->getNamespace() == NS_FILE ) {
3454  $title,
3455  'imagelinks',
3456  [ 'causeAction' => 'page-delete' ]
3457  );
3458  JobQueueGroup::singleton()->lazyPush( $job );
3459  }
3460 
3461  // User talk pages
3462  if ( $title->getNamespace() == NS_USER_TALK ) {
3463  $user = User::newFromName( $title->getText(), false );
3464  if ( $user ) {
3465  $user->setNewtalk( false );
3466  }
3467  }
3468 
3469  // Image redirects
3470  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3471 
3472  // Purge cross-wiki cache entities referencing this page
3473  self::purgeInterwikiCheckKey( $title );
3474  }
3475 
3484  public static function onArticleEdit(
3485  Title $title,
3486  Revision $revision = null,
3487  $slotsChanged = null
3488  ) {
3489  // TODO: move this into a PageEventEmitter service
3490  $jobs = [];
3491  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3492  // Invalidate caches of articles which include this page.
3493  // Only for the main slot, because only the main slot is transcluded.
3494  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3496  $title,
3497  'templatelinks',
3498  [ 'causeAction' => 'page-edit' ]
3499  );
3500  }
3501  // Invalidate the caches of all pages which redirect here
3503  $title,
3504  'redirect',
3505  [ 'causeAction' => 'page-edit' ]
3506  );
3507  JobQueueGroup::singleton()->lazyPush( $jobs );
3508 
3509  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3510 
3511  // Purge CDN for this page only
3512  $title->purgeSquid();
3513  // Clear file cache for this page only
3515 
3516  // Purge ?action=info cache
3517  $revid = $revision ? $revision->getId() : null;
3518  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3519  InfoAction::invalidateCache( $title, $revid );
3520  } );
3521 
3522  // Purge cross-wiki cache entities referencing this page
3523  self::purgeInterwikiCheckKey( $title );
3524  }
3525 
3533  private static function purgeInterwikiCheckKey( Title $title ) {
3535 
3536  if ( !$wgEnableScaryTranscluding ) {
3537  return; // @todo: perhaps this wiki is only used as a *source* for content?
3538  }
3539 
3540  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3541  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3542  $cache->resetCheckKey(
3543  // Do not include the namespace since there can be multiple aliases to it
3544  // due to different namespace text definitions on different wikis. This only
3545  // means that some cache invalidations happen that are not strictly needed.
3546  $cache->makeGlobalKey(
3547  'interwiki-page',
3549  $title->getDBkey()
3550  )
3551  );
3552  } );
3553  }
3554 
3561  public function getCategories() {
3562  $id = $this->getId();
3563  if ( $id == 0 ) {
3564  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3565  }
3566 
3567  $dbr = wfGetDB( DB_REPLICA );
3568  $res = $dbr->select( 'categorylinks',
3569  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3570  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3571  // as not being aliases, and NS_CATEGORY is numeric
3572  [ 'cl_from' => $id ],
3573  __METHOD__ );
3574 
3575  return TitleArray::newFromResult( $res );
3576  }
3577 
3584  public function getHiddenCategories() {
3585  $result = [];
3586  $id = $this->getId();
3587 
3588  if ( $id == 0 ) {
3589  return [];
3590  }
3591 
3592  $dbr = wfGetDB( DB_REPLICA );
3593  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3594  [ 'cl_to' ],
3595  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3596  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3597  __METHOD__ );
3598 
3599  if ( $res !== false ) {
3600  foreach ( $res as $row ) {
3601  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3602  }
3603  }
3604 
3605  return $result;
3606  }
3607 
3615  public function getAutoDeleteReason( &$hasHistory ) {
3616  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3617  }
3618 
3629  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3630  $id = $id ?: $this->getId();
3631  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3632  getCategoryLinkType( $this->getTitle()->getNamespace() );
3633 
3634  $addFields = [ 'cat_pages = cat_pages + 1' ];
3635  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3636  if ( $type !== 'page' ) {
3637  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3638  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3639  }
3640 
3641  $dbw = wfGetDB( DB_MASTER );
3642 
3643  if ( count( $added ) ) {
3644  $existingAdded = $dbw->selectFieldValues(
3645  'category',
3646  'cat_title',
3647  [ 'cat_title' => $added ],
3648  __METHOD__
3649  );
3650 
3651  // For category rows that already exist, do a plain
3652  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3653  // to avoid creating gaps in the cat_id sequence.
3654  if ( count( $existingAdded ) ) {
3655  $dbw->update(
3656  'category',
3657  $addFields,
3658  [ 'cat_title' => $existingAdded ],
3659  __METHOD__
3660  );
3661  }
3662 
3663  $missingAdded = array_diff( $added, $existingAdded );
3664  if ( count( $missingAdded ) ) {
3665  $insertRows = [];
3666  foreach ( $missingAdded as $cat ) {
3667  $insertRows[] = [
3668  'cat_title' => $cat,
3669  'cat_pages' => 1,
3670  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3671  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3672  ];
3673  }
3674  $dbw->upsert(
3675  'category',
3676  $insertRows,
3677  [ 'cat_title' ],
3678  $addFields,
3679  __METHOD__
3680  );
3681  }
3682  }
3683 
3684  if ( count( $deleted ) ) {
3685  $dbw->update(
3686  'category',
3687  $removeFields,
3688  [ 'cat_title' => $deleted ],
3689  __METHOD__
3690  );
3691  }
3692 
3693  foreach ( $added as $catName ) {
3694  $cat = Category::newFromName( $catName );
3695  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3696  }
3697 
3698  foreach ( $deleted as $catName ) {
3699  $cat = Category::newFromName( $catName );
3700  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3701  // Refresh counts on categories that should be empty now (after commit, T166757)
3702  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3703  $cat->refreshCountsIfEmpty();
3704  } );
3705  }
3706  }
3707 
3714  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3715  if ( wfReadOnly() ) {
3716  return;
3717  }
3718 
3719  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3720  [ $this, $this->mTitle, $parserOutput ]
3721  ) ) {
3722  return;
3723  }
3724 
3725  $config = RequestContext::getMain()->getConfig();
3726 
3727  $params = [
3728  'isOpportunistic' => true,
3729  'rootJobTimestamp' => $parserOutput->getCacheTime()
3730  ];
3731 
3732  if ( $this->mTitle->areRestrictionsCascading() ) {
3733  // If the page is cascade protecting, the links should really be up-to-date
3734  JobQueueGroup::singleton()->lazyPush(
3735  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3736  );
3737  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3738  // Assume the output contains "dynamic" time/random based magic words.
3739  // Only update pages that expired due to dynamic content and NOT due to edits
3740  // to referenced templates/files. When the cache expires due to dynamic content,
3741  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3742  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3743  // template/file edit already triggered recursive RefreshLinksJob jobs.
3744  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3745  // If a page is uncacheable, do not keep spamming a job for it.
3746  // Although it would be de-duplicated, it would still waste I/O.
3748  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3749  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3750  if ( $cache->add( $key, time(), $ttl ) ) {
3751  JobQueueGroup::singleton()->lazyPush(
3752  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3753  );
3754  }
3755  }
3756  }
3757  }
3758 
3768  public function getDeletionUpdates( $rev = null ) {
3769  if ( !$rev ) {
3770  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3771 
3772  try {
3773  $rev = $this->getRevisionRecord();
3774  } catch ( Exception $ex ) {
3775  // If we can't load the content, something is wrong. Perhaps that's why
3776  // the user is trying to delete the page, so let's not fail in that case.
3777  // Note that doDeleteArticleReal() will already have logged an issue with
3778  // loading the content.
3779  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3780  }
3781  }
3782 
3783  if ( !$rev ) {
3784  $slotContent = [];
3785  } elseif ( $rev instanceof Content ) {
3786  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3787 
3788  $slotContent = [ SlotRecord::MAIN => $rev ];
3789  } else {
3790  $slotContent = array_map( function ( SlotRecord $slot ) {
3791  return $slot->getContent();
3792  }, $rev->getSlots()->getSlots() );
3793  }
3794 
3795  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3796 
3797  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3798  // model here, not the content object!
3799  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3801  foreach ( $slotContent as $role => $content ) {
3802  $handler = $content->getContentHandler();
3803 
3804  $updates = $handler->getDeletionUpdates(
3805  $this->getTitle(),
3806  $role
3807  );
3808  $allUpdates = array_merge( $allUpdates, $updates );
3809 
3810  // TODO: remove B/C hack in 1.32!
3811  $legacyUpdates = $content->getDeletionUpdates( $this );
3812 
3813  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3814  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3815  return !( $update instanceof LinksDeletionUpdate );
3816  } );
3817 
3818  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3819  }
3820 
3821  Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3822 
3823  // TODO: hard deprecate old hook in 1.33
3824  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3825  return $allUpdates;
3826  }
3827 
3835  public function isLocal() {
3836  return true;
3837  }
3838 
3848  public function getWikiDisplayName() {
3849  global $wgSitename;
3850  return $wgSitename;
3851  }
3852 
3861  public function getSourceURL() {
3862  return $this->getTitle()->getCanonicalURL();
3863  }
3864 
3871  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3872 
3873  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3874  }
3875 
3876 }
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:703
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:264
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3009
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2088
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
setLastEdit(Revision $revision)
Set the latest revision.
Definition: WikiPage.php:778
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:3391
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4341
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article...
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1742
string $mLinksUpdated
Definition: WikiPage.php:111
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
static newFromName( $name)
Factory function.
Definition: Category.php:126
static getRequestId()
Get the unique request ID.
Definition: WebRequest.php:309
getLatest()
Get the page_latest field.
Definition: WikiPage.php:714
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3848
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:489
$context
Definition: load.php:45
getParserCache()
Definition: WikiPage.php:260
string $mTouched
Definition: WikiPage.php:106
$wgSitename
Name of the site.
$wgUseAutomaticEditSummaries
If user doesn&#39;t specify any edit summary when making a an edit, MediaWiki will try to automatically c...
$success
getRevisionRecord()
Definition: Revision.php:434
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:996
clearNotification(&$title, $oldid=0)
Clear the user&#39;s notification timestamp for the given title.
Definition: User.php:3752
int $mId
Definition: WikiPage.php:81
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:799
static newFromTitle( $title)
Factory function.
Definition: Category.php:146
getDBLoadBalancer()
Definition: WikiPage.php:267
int $wgMultiContentRevisionSchemaMigrationStage
RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables)...
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:86
updateCategoryCounts(array $added, array $deleted, $id=0)
Update all the appropriate counts in the category table, given that we&#39;ve added the categories $added...
Definition: WikiPage.php:3629
getTimestamp()
Definition: Revision.php:799
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2498
The Message class provides methods which fulfil two basic services:
Definition: Message.php:162
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1046
Title $mTitle
Definition: WikiPage.php:53
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3533
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1123
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
const EDIT_INTERNAL
Definition: Defines.php:139
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:297
clear()
Clear the object.
Definition: WikiPage.php:306
Handles purging the appropriate CDN objects given a list of URLs or Title instances.
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:617
$wgEnableScaryTranscluding
Enable interwiki transcluding.
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3714
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3861
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1339
$source
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:929
static newFromPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given page ID...
Definition: Revision.php:158
getOtherPage()
Get the other title for this page, if this is a subject page get the talk page, if it is a subject pa...
Definition: Title.php:1706
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:78
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2674
static getLocalClusterInstance()
Get the main cluster-local cache object.
Value object representing a modification of revision slots.
getContributors()
Get a list of users who have edited this article, not including the user who made the most recent rev...
Definition: WikiPage.php:1161
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
newDerivedDataUpdater()
Definition: WikiPage.php:1696
const EDIT_MINOR
Definition: Defines.php:134
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
const EDIT_UPDATE
Definition: Defines.php:133
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:516
getTouched()
Get the page_touched field.
Definition: WikiPage.php:692
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
setTimestamp( $ts)
Set the page timestamp (use only to avoid DB queries)
Definition: WikiPage.php:845
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition: WikiPage.php:101
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1635
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
const DB_MASTER
Definition: defines.php:26
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:317
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
loadLastEdit()
Loads everything except the text This isn&#39;t necessary for all uses, so it&#39;s only done if needed...
Definition: WikiPage.php:738
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:116
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3870
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2287
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object&#39;s namespace and title using a sanitized title string...
Definition: WikiPage.php:459
getActionOverrides()
Definition: WikiPage.php:277
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:634
Class DeletePageJob.
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
getRevisionStore()
Definition: WikiPage.php:239
getContentModel()
Returns the page&#39;s content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:652
static onArticleEdit(Title $title, Revision $revision=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3484
static newCanonical( $context=null, $userLang=null)
Creates a "canonical" ParserOptions object.
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1463
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:681
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility...
static get(Title $title)
Create a new BacklinkCache or reuse any existing one.
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:3144
doDeleteArticleReal( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $deleter=null, $tags=[], $logsubtype='delete', $immediate=false)
Back-end article deletion Deletes the article with database consistency, writes logs, purges caches.
Definition: WikiPage.php:2628
getRevision()
Get the latest revision.
Definition: WikiPage.php:787
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
if( $line===false) $args
Definition: mcc.php:124
getRevisionRenderer()
Definition: WikiPage.php:246
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
newPageUpdater(User $user, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
Definition: WikiPage.php:1801
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:29
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:71
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:129
wfReadOnly()
Check whether the wiki is in read-only mode.
wfIncrStats( $key, $count=1)
Increment a statistics counter.
static newMigration()
Static constructor.
deleteTitleProtection()
Remove any title protection due to page existing.
Definition: Title.php:2563
static getMain()
Get the RequestContext object associated with the main request.
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:2566
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
const EDIT_FORCE_BOT
Definition: Defines.php:136
static clearFileCache(Title $title)
Clear the file caches for a page for all actions.
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:2595
doDeleteUpdates( $id, Content $content=null, Revision $revision=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:3015
Revision $mLastRevision
Definition: WikiPage.php:96
getUserText( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:896
getDBkey()
Get the main part with underscores.
Definition: Title.php:1014
updateIfNewerOn( $dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record...
Definition: WikiPage.php:1499
$wgAjaxEditStash
Have clients send edits to be prepared when filling in edit summaries.
static factory(array $deltas)
static newKnownCurrent(IDatabase $db, $pageIdOrTitle, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: Revision.php:1124
getSlotRoleRegistry()
Definition: WikiPage.php:253
static newForBacklinks(Title $title, $table, $params=[])
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1112
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1596
const NS_MEDIA
Definition: Defines.php:48
static singleton()
Definition: RepoGroup.php:60
getContentHandler()
Returns the content handler appropriate for this revision&#39;s content model.
Definition: Revision.php:792
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:130
$wgRCWatchCategoryMembership
Treat category membership changes as a RecentChange.
getCacheTime()
Definition: CacheTime.php:60
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:725
$cache
Definition: mcc.php:33
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3584
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1274
getTitle()
Get the title object of the article.
Definition: WikiPage.php:298
const NS_CATEGORY
Definition: Defines.php:74
getComment( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:915
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:63
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
static newFromResult( $res)
Definition: TitleArray.php:42
hasViewableContent()
Check if this page is something we&#39;re going to be showing some sort of sensible content for...
Definition: WikiPage.php:625
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object...
Definition: Revision.php:316
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1035
const NS_FILE
Definition: Defines.php:66
int false $mLatest
False means "not loaded".
Definition: WikiPage.php:71
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:924
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:560
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history...
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:592
Special handling for file pages.
const NS_MEDIAWIKI
Definition: Defines.php:68
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:290
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1297
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2132
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3615
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2433
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:1556
bool $mDataLoaded
Definition: WikiPage.php:59
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
static invalidateModuleCache(Title $title, ?Revision $old, ?Revision $new, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
$wgPageCreationLog
Maintain a log of page creations at Special:Log/create?
getStubThreshold()
Thumb size preferred by the user.
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1961
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:946
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:1991
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
static queueRecursiveJobsForTable(Title $title, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:180
static newDynamic(Title $title, array $params)
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:560
A handle for managing updates for derived page data on edit, import, purge, etc.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
wfRandom()
Get a random decimal value in the domain of [0, 1), in a way not likely to give duplicate values for ...
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
static getStore()
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:3561
static convertSelectType( $type)
Convert &#39;fromdb&#39;, &#39;fromdbmaster&#39; and &#39;forupdate&#39; to READ_* constants.
Definition: WikiPage.php:222
hasDynamicContent()
Check whether the cache TTL was lowered due to dynamic content.
doEditUpdates(Revision $revision, User $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2060
getId()
Get the user&#39;s ID.
Definition: User.php:2258
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4452
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1964
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3835
const EDIT_NEW
Definition: Defines.php:132
getTimestamp()
Definition: WikiPage.php:831
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:473
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:1745
static hasDifferencesOutsideMainSlot(Revision $a, Revision $b)
Helper method for checking whether two revisions have differences that go beyond the main slot...
Definition: WikiPage.php:1537
getUser( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:858
Controller-like object for creating and updating pages by creating new revisions. ...
Definition: PageUpdater.php:73
Overloads the relevant methods of the real ResultsWrapper so it doesn&#39;t go anywhere near an actual da...
getDeletionUpdates( $rev=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3768
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3427
if(count( $args)< 1) $job
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
supportsSections()
Returns true if this page&#39;s content model supports sections.
Definition: WikiPage.php:1578
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:3098
static newPrioritized(Title $title, array $params)
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1681
$revQuery
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:426
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:820
PreparedEdit false $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:76
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1211
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:999
formatExpiry( $expiry)
Definition: WikiPage.php:2476
bool $mIsRedirect
Definition: WikiPage.php:65
static singleton( $domain=false)
insertRedirectEntry(Title $rt, $oldLatest=null)
Insert or update the redirect table entry for this page to indicate it redirects to $rt...
Definition: WikiPage.php:1072
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1233
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1384
Page revision base class.
isSafeToCache()
Test whether these options are safe to cache.
const DB_REPLICA
Definition: defines.php:25
$content
Definition: router.php:78
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2987
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:1882
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:536
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:120
static selectFields()
Return the list of revision fields that should be selected to create a new page.
Definition: WikiPage.php:348
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:337
const PRC_AUTOPATROLLED
const NS_USER_TALK
Definition: Defines.php:63
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Title $mRedirectTarget
Definition: WikiPage.php:91
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:122
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page&#39;s history.
Definition: Revision.php:995
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object...
Definition: WikiPage.php:387
getCreator( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:877
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:2841
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:210
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:534
Special handling for category pages.
static singleton()
Get the singleton instance of this class.
purgeSquid()
Purge all applicable CDN URLs.
Definition: Title.php:3574
doUpdateRestrictions(array $limit, array $expiry, &$cascade, $reason, User $user, $tags=null)
Update the article&#39;s restriction field, and leave a log entry.
Definition: WikiPage.php:2162
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2540