MediaWiki  1.34.0
WikiPage.php
Go to the documentation of this file.
1 <?php
34 use Wikimedia\Assert\Assert;
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 
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' ) );
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  // Update restrictions table
2296  foreach ( $limit as $action => $restrictions ) {
2297  $dbw->delete(
2298  'page_restrictions',
2299  [
2300  'pr_page' => $id,
2301  'pr_type' => $action
2302  ],
2303  __METHOD__
2304  );
2305  if ( $restrictions != '' ) {
2306  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2307  $dbw->insert(
2308  'page_restrictions',
2309  [
2310  'pr_page' => $id,
2311  'pr_type' => $action,
2312  'pr_level' => $restrictions,
2313  'pr_cascade' => $cascadeValue,
2314  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2315  ],
2316  __METHOD__
2317  );
2318  $logRelationsValues[] = $dbw->insertId();
2319  $logParamsDetails[] = [
2320  'type' => $action,
2321  'level' => $restrictions,
2322  'expiry' => $expiry[$action],
2323  'cascade' => (bool)$cascadeValue,
2324  ];
2325  }
2326  }
2327 
2328  // Clear out legacy restriction fields
2329  $dbw->update(
2330  'page',
2331  [ 'page_restrictions' => '' ],
2332  [ 'page_id' => $id ],
2333  __METHOD__
2334  );
2335 
2336  // Avoid PHP 7.1 warning of passing $this by reference
2337  $wikiPage = $this;
2338 
2339  Hooks::run( 'NewRevisionFromEditComplete',
2340  [ $this, $nullRevision, $latest, $user ] );
2341  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2342  } else { // Protection of non-existing page (also known as "title protection")
2343  // Cascade protection is meaningless in this case
2344  $cascade = false;
2345 
2346  if ( $limit['create'] != '' ) {
2347  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2348  $dbw->replace( 'protected_titles',
2349  [ [ 'pt_namespace', 'pt_title' ] ],
2350  [
2351  'pt_namespace' => $this->mTitle->getNamespace(),
2352  'pt_title' => $this->mTitle->getDBkey(),
2353  'pt_create_perm' => $limit['create'],
2354  'pt_timestamp' => $dbw->timestamp(),
2355  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2356  'pt_user' => $user->getId(),
2357  ] + $commentFields, __METHOD__
2358  );
2359  $logParamsDetails[] = [
2360  'type' => 'create',
2361  'level' => $limit['create'],
2362  'expiry' => $expiry['create'],
2363  ];
2364  } else {
2365  $dbw->delete( 'protected_titles',
2366  [
2367  'pt_namespace' => $this->mTitle->getNamespace(),
2368  'pt_title' => $this->mTitle->getDBkey()
2369  ], __METHOD__
2370  );
2371  }
2372  }
2373 
2374  $this->mTitle->flushRestrictions();
2375  InfoAction::invalidateCache( $this->mTitle );
2376 
2377  if ( $logAction == 'unprotect' ) {
2378  $params = [];
2379  } else {
2380  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2381  $params = [
2382  '4::description' => $protectDescriptionLog, // parameter for IRC
2383  '5:bool:cascade' => $cascade,
2384  'details' => $logParamsDetails, // parameter for localize and api
2385  ];
2386  }
2387 
2388  // Update the protection log
2389  $logEntry = new ManualLogEntry( 'protect', $logAction );
2390  $logEntry->setTarget( $this->mTitle );
2391  $logEntry->setComment( $reason );
2392  $logEntry->setPerformer( $user );
2393  $logEntry->setParameters( $params );
2394  if ( !is_null( $nullRevision ) ) {
2395  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2396  }
2397  $logEntry->addTags( $tags );
2398  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2399  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2400  }
2401  $logId = $logEntry->insert();
2402  $logEntry->publish( $logId );
2403 
2404  return Status::newGood( $logId );
2405  }
2406 
2418  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2419  array $expiry, $cascade, $reason, $user = null
2420  ) {
2421  $dbw = wfGetDB( DB_MASTER );
2422 
2423  // Prepare a null revision to be added to the history
2424  $editComment = wfMessage(
2425  $revCommentMsg,
2426  $this->mTitle->getPrefixedText(),
2427  $user ? $user->getName() : ''
2428  )->inContentLanguage()->text();
2429  if ( $reason ) {
2430  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2431  }
2432  $protectDescription = $this->protectDescription( $limit, $expiry );
2433  if ( $protectDescription ) {
2434  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2435  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2436  ->inContentLanguage()->text();
2437  }
2438  if ( $cascade ) {
2439  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2440  $editComment .= wfMessage( 'brackets' )->params(
2441  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2442  )->inContentLanguage()->text();
2443  }
2444 
2445  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2446  if ( $nullRev ) {
2447  $nullRev->insertOn( $dbw );
2448 
2449  // Update page record and touch page
2450  $oldLatest = $nullRev->getParentId();
2451  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2452  }
2453 
2454  return $nullRev;
2455  }
2456 
2461  protected function formatExpiry( $expiry ) {
2462  if ( $expiry != 'infinity' ) {
2463  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2464  return wfMessage(
2465  'protect-expiring',
2466  $contLang->timeanddate( $expiry, false, false ),
2467  $contLang->date( $expiry, false, false ),
2468  $contLang->time( $expiry, false, false )
2469  )->inContentLanguage()->text();
2470  } else {
2471  return wfMessage( 'protect-expiry-indefinite' )
2472  ->inContentLanguage()->text();
2473  }
2474  }
2475 
2483  public function protectDescription( array $limit, array $expiry ) {
2484  $protectDescription = '';
2485 
2486  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2487  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2488  # All possible message keys are listed here for easier grepping:
2489  # * restriction-create
2490  # * restriction-edit
2491  # * restriction-move
2492  # * restriction-upload
2493  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2494  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2495  # with '' filtered out. All possible message keys are listed below:
2496  # * protect-level-autoconfirmed
2497  # * protect-level-sysop
2498  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2499  ->inContentLanguage()->text();
2500 
2501  $expiryText = $this->formatExpiry( $expiry[$action] );
2502 
2503  if ( $protectDescription !== '' ) {
2504  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2505  }
2506  $protectDescription .= wfMessage( 'protect-summary-desc' )
2507  ->params( $actionText, $restrictionsText, $expiryText )
2508  ->inContentLanguage()->text();
2509  }
2510 
2511  return $protectDescription;
2512  }
2513 
2525  public function protectDescriptionLog( array $limit, array $expiry ) {
2526  $protectDescriptionLog = '';
2527 
2528  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2529  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2530  $expiryText = $this->formatExpiry( $expiry[$action] );
2531  $protectDescriptionLog .=
2532  $dirMark .
2533  "[$action=$restrictions] ($expiryText)";
2534  }
2535 
2536  return trim( $protectDescriptionLog );
2537  }
2538 
2548  protected static function flattenRestrictions( $limit ) {
2549  if ( !is_array( $limit ) ) {
2550  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2551  }
2552 
2553  $bits = [];
2554  ksort( $limit );
2555 
2556  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2557  $bits[] = "$action=$restrictions";
2558  }
2559 
2560  return implode( ':', $bits );
2561  }
2562 
2575  public function isBatchedDelete( $safetyMargin = 0 ) {
2577 
2578  $dbr = wfGetDB( DB_REPLICA );
2579  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2580  $revCount += $safetyMargin;
2581 
2582  return $revCount >= $wgDeleteRevisionsBatchSize;
2583  }
2584 
2604  public function doDeleteArticle(
2605  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2606  $immediate = false
2607  ) {
2608  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2609  [], 'delete', $immediate );
2610 
2611  // Returns true if the page was actually deleted, or is scheduled for deletion
2612  return $status->isOK();
2613  }
2614 
2637  public function doDeleteArticleReal(
2638  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2639  $tags = [], $logsubtype = 'delete', $immediate = false
2640  ) {
2641  global $wgUser;
2642 
2643  wfDebug( __METHOD__ . "\n" );
2644 
2646 
2647  // Avoid PHP 7.1 warning of passing $this by reference
2648  $wikiPage = $this;
2649 
2650  if ( !$deleter ) {
2651  $deleter = $wgUser;
2652  }
2653  if ( !Hooks::run( 'ArticleDelete',
2654  [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2655  ) ) {
2656  if ( $status->isOK() ) {
2657  // Hook aborted but didn't set a fatal status
2658  $status->fatal( 'delete-hook-aborted' );
2659  }
2660  return $status;
2661  }
2662 
2663  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2664  $logsubtype, $immediate );
2665  }
2666 
2675  public function doDeleteArticleBatched(
2676  $reason, $suppress, User $deleter, $tags,
2677  $logsubtype, $immediate = false, $webRequestId = null
2678  ) {
2679  wfDebug( __METHOD__ . "\n" );
2680 
2682 
2683  $dbw = wfGetDB( DB_MASTER );
2684  $dbw->startAtomic( __METHOD__ );
2685 
2686  $this->loadPageData( self::READ_LATEST );
2687  $id = $this->getId();
2688  // T98706: lock the page from various other updates but avoid using
2689  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2690  // the revisions queries (which also JOIN on user). Only lock the page
2691  // row and CAS check on page_latest to see if the trx snapshot matches.
2692  $lockedLatest = $this->lockAndGetLatest();
2693  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2694  $dbw->endAtomic( __METHOD__ );
2695  // Page not there or trx snapshot is stale
2696  $status->error( 'cannotdelete',
2697  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2698  return $status;
2699  }
2700 
2701  // At this point we are now committed to returning an OK
2702  // status unless some DB query error or other exception comes up.
2703  // This way callers don't have to call rollback() if $status is bad
2704  // unless they actually try to catch exceptions (which is rare).
2705 
2706  // we need to remember the old content so we can use it to generate all deletion updates.
2707  $revision = $this->getRevision();
2708  try {
2709  $content = $this->getContent( RevisionRecord::RAW );
2710  } catch ( Exception $ex ) {
2711  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2712  . $ex->getMessage() );
2713 
2714  $content = null;
2715  }
2716 
2717  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2718  // one batch of revisions and defer archival of any others to the job queue.
2719  $explictTrxLogged = false;
2720  while ( true ) {
2721  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2722  if ( $done || !$immediate ) {
2723  break;
2724  }
2725  $dbw->endAtomic( __METHOD__ );
2726  if ( $dbw->explicitTrxActive() ) {
2727  // Explict transactions may never happen here in practice. Log to be sure.
2728  if ( !$explictTrxLogged ) {
2729  $explictTrxLogged = true;
2730  LoggerFactory::getInstance( 'wfDebug' )->debug(
2731  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2732  'title' => $this->getTitle()->getText(),
2733  ] );
2734  }
2735  continue;
2736  }
2737  if ( $dbw->trxLevel() ) {
2738  $dbw->commit();
2739  }
2740  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2741  $lbFactory->waitForReplication();
2742  $dbw->startAtomic( __METHOD__ );
2743  }
2744 
2745  // If done archiving, also delete the article.
2746  if ( !$done ) {
2747  $dbw->endAtomic( __METHOD__ );
2748 
2749  $jobParams = [
2750  'namespace' => $this->getTitle()->getNamespace(),
2751  'title' => $this->getTitle()->getDBkey(),
2752  'wikiPageId' => $id,
2753  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2754  'reason' => $reason,
2755  'suppress' => $suppress,
2756  'userId' => $deleter->getId(),
2757  'tags' => json_encode( $tags ),
2758  'logsubtype' => $logsubtype,
2759  ];
2760 
2761  $job = new DeletePageJob( $jobParams );
2762  JobQueueGroup::singleton()->push( $job );
2763 
2764  $status->warning( 'delete-scheduled',
2765  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2766  } else {
2767  // Get archivedRevisionCount by db query, because there's no better alternative.
2768  // Jobs cannot pass a count of archived revisions to the next job, because additional
2769  // deletion operations can be started while the first is running. Jobs from each
2770  // gracefully interleave, but would not know about each other's count. Deduplication
2771  // in the job queue to avoid simultaneous deletion operations would add overhead.
2772  // Number of archived revisions cannot be known beforehand, because edits can be made
2773  // while deletion operations are being processed, changing the number of archivals.
2774  $archivedRevisionCount = (int)$dbw->selectField(
2775  'archive', 'COUNT(*)',
2776  [
2777  'ar_namespace' => $this->getTitle()->getNamespace(),
2778  'ar_title' => $this->getTitle()->getDBkey(),
2779  'ar_page_id' => $id
2780  ], __METHOD__
2781  );
2782 
2783  // Clone the title and wikiPage, so we have the information we need when
2784  // we log and run the ArticleDeleteComplete hook.
2785  $logTitle = clone $this->mTitle;
2786  $wikiPageBeforeDelete = clone $this;
2787 
2788  // Now that it's safely backed up, delete it
2789  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2790 
2791  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2792  $logtype = $suppress ? 'suppress' : 'delete';
2793 
2794  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2795  $logEntry->setPerformer( $deleter );
2796  $logEntry->setTarget( $logTitle );
2797  $logEntry->setComment( $reason );
2798  $logEntry->addTags( $tags );
2799  $logid = $logEntry->insert();
2800 
2801  $dbw->onTransactionPreCommitOrIdle(
2802  function () use ( $logEntry, $logid ) {
2803  // T58776: avoid deadlocks (especially from FileDeleteForm)
2804  $logEntry->publish( $logid );
2805  },
2806  __METHOD__
2807  );
2808 
2809  $dbw->endAtomic( __METHOD__ );
2810 
2811  $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2812 
2813  Hooks::run( 'ArticleDeleteComplete', [
2814  &$wikiPageBeforeDelete,
2815  &$deleter,
2816  $reason,
2817  $id,
2818  $content,
2819  $logEntry,
2820  $archivedRevisionCount
2821  ] );
2822  $status->value = $logid;
2823 
2824  // Show log excerpt on 404 pages rather than just a link
2825  $dbCache = ObjectCache::getInstance( 'db-replicated' );
2826  $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2827  $dbCache->set( $key, 1, $dbCache::TTL_DAY );
2828  }
2829 
2830  return $status;
2831  }
2832 
2842  protected function archiveRevisions( $dbw, $id, $suppress ) {
2845 
2846  // Given the lock above, we can be confident in the title and page ID values
2847  $namespace = $this->getTitle()->getNamespace();
2848  $dbKey = $this->getTitle()->getDBkey();
2849 
2850  $commentStore = CommentStore::getStore();
2851  $actorMigration = ActorMigration::newMigration();
2852 
2854  $bitfield = false;
2855 
2856  // Bitfields to further suppress the content
2857  if ( $suppress ) {
2858  $bitfield = RevisionRecord::SUPPRESSED_ALL;
2859  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2860  }
2861 
2862  // For now, shunt the revision data into the archive table.
2863  // Text is *not* removed from the text table; bulk storage
2864  // is left intact to avoid breaking block-compression or
2865  // immutable storage schemes.
2866  // In the future, we may keep revisions and mark them with
2867  // the rev_deleted field, which is reserved for this purpose.
2868 
2869  // Lock rows in `revision` and its temp tables, but not any others.
2870  // Note array_intersect() preserves keys from the first arg, and we're
2871  // assuming $revQuery has `revision` primary and isn't using subtables
2872  // for anything we care about.
2873  $dbw->lockForUpdate(
2874  array_intersect(
2875  $revQuery['tables'],
2876  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2877  ),
2878  [ 'rev_page' => $id ],
2879  __METHOD__,
2880  [],
2881  $revQuery['joins']
2882  );
2883 
2884  // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
2885  // so we can copy it to the archive table.
2886  // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
2888  $revQuery['fields'][] = 'rev_text_id';
2889 
2890  if ( $wgContentHandlerUseDB ) {
2891  $revQuery['fields'][] = 'rev_content_model';
2892  $revQuery['fields'][] = 'rev_content_format';
2893  }
2894  }
2895 
2896  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2897  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2898  $res = $dbw->select(
2899  $revQuery['tables'],
2900  $revQuery['fields'],
2901  [ 'rev_page' => $id ],
2902  __METHOD__,
2903  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2904  $revQuery['joins']
2905  );
2906 
2907  // Build their equivalent archive rows
2908  $rowsInsert = [];
2909  $revids = [];
2910 
2912  $ipRevIds = [];
2913 
2914  $done = true;
2915  foreach ( $res as $row ) {
2916  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2917  $done = false;
2918  break;
2919  }
2920 
2921  $comment = $commentStore->getComment( 'rev_comment', $row );
2922  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2923  $rowInsert = [
2924  'ar_namespace' => $namespace,
2925  'ar_title' => $dbKey,
2926  'ar_timestamp' => $row->rev_timestamp,
2927  'ar_minor_edit' => $row->rev_minor_edit,
2928  'ar_rev_id' => $row->rev_id,
2929  'ar_parent_id' => $row->rev_parent_id,
2938  'ar_len' => $row->rev_len,
2939  'ar_page_id' => $id,
2940  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2941  'ar_sha1' => $row->rev_sha1,
2942  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2943  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2944 
2946  $rowInsert['ar_text_id'] = $row->rev_text_id;
2947 
2948  if ( $wgContentHandlerUseDB ) {
2949  $rowInsert['ar_content_model'] = $row->rev_content_model;
2950  $rowInsert['ar_content_format'] = $row->rev_content_format;
2951  }
2952  }
2953 
2954  $rowsInsert[] = $rowInsert;
2955  $revids[] = $row->rev_id;
2956 
2957  // Keep track of IP edits, so that the corresponding rows can
2958  // be deleted in the ip_changes table.
2959  if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2960  $ipRevIds[] = $row->rev_id;
2961  }
2962  }
2963 
2964  // This conditional is just a sanity check
2965  if ( count( $revids ) > 0 ) {
2966  // Copy them into the archive table
2967  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2968 
2969  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2970  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2971  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2972 
2973  // Also delete records from ip_changes as applicable.
2974  if ( count( $ipRevIds ) > 0 ) {
2975  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2976  }
2977  }
2978 
2979  return $done;
2980  }
2981 
2988  public function lockAndGetLatest() {
2989  return (int)wfGetDB( DB_MASTER )->selectField(
2990  'page',
2991  'page_latest',
2992  [
2993  'page_id' => $this->getId(),
2994  // Typically page_id is enough, but some code might try to do
2995  // updates assuming the title is the same, so verify that
2996  'page_namespace' => $this->getTitle()->getNamespace(),
2997  'page_title' => $this->getTitle()->getDBkey()
2998  ],
2999  __METHOD__,
3000  [ 'FOR UPDATE' ]
3001  );
3002  }
3003 
3016  public function doDeleteUpdates(
3017  $id, Content $content = null, Revision $revision = null, User $user = null
3018  ) {
3019  if ( $id !== $this->getId() ) {
3020  throw new InvalidArgumentException( 'Mismatching page ID' );
3021  }
3022 
3023  try {
3024  $countable = $this->isCountable();
3025  } catch ( Exception $ex ) {
3026  // fallback for deleting broken pages for which we cannot load the content for
3027  // some reason. Note that doDeleteArticleReal() already logged this problem.
3028  $countable = false;
3029  }
3030 
3031  // Update site status
3033  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3034  ) );
3035 
3036  // Delete pagelinks, update secondary indexes, etc
3037  $updates = $this->getDeletionUpdates(
3038  $revision ? $revision->getRevisionRecord() : $content
3039  );
3040  foreach ( $updates as $update ) {
3041  DeferredUpdates::addUpdate( $update );
3042  }
3043 
3044  $causeAgent = $user ? $user->getName() : 'unknown';
3045  // Reparse any pages transcluding this page
3047  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3048  // Reparse any pages including this image
3049  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3051  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3052  }
3053 
3054  // Clear caches
3055  self::onArticleDelete( $this->mTitle );
3057  $this->mTitle,
3058  $revision,
3059  null,
3061  );
3062 
3063  // Reset this object and the Title object
3064  $this->loadFromRow( false, self::READ_LATEST );
3065 
3066  // Search engine
3067  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3068  }
3069 
3099  public function doRollback(
3100  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3101  ) {
3102  $resultDetails = null;
3103 
3104  // Check permissions
3105  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3106  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3107  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3108 
3109  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3110  $errors[] = [ 'sessionfailure' ];
3111  }
3112 
3113  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3114  $errors[] = [ 'actionthrottledtext' ];
3115  }
3116 
3117  // If there were errors, bail out now
3118  if ( !empty( $errors ) ) {
3119  return $errors;
3120  }
3121 
3122  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3123  }
3124 
3145  public function commitRollback( $fromP, $summary, $bot,
3146  &$resultDetails, User $guser, $tags = null
3147  ) {
3149 
3150  $dbw = wfGetDB( DB_MASTER );
3151 
3152  if ( wfReadOnly() ) {
3153  return [ [ 'readonlytext' ] ];
3154  }
3155 
3156  // Begin revision creation cycle by creating a PageUpdater.
3157  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3158  $updater = $this->newPageUpdater( $guser );
3159  $current = $updater->grabParentRevision();
3160 
3161  if ( is_null( $current ) ) {
3162  // Something wrong... no page?
3163  return [ [ 'notanarticle' ] ];
3164  }
3165 
3166  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3167  $legacyCurrent = new Revision( $current );
3168  $from = str_replace( '_', ' ', $fromP );
3169 
3170  // User name given should match up with the top revision.
3171  // If the revision's user is not visible, then $from should be empty.
3172  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3173  $resultDetails = [ 'current' => $legacyCurrent ];
3174  return [ [ 'alreadyrolled',
3175  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3176  htmlspecialchars( $fromP ),
3177  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3178  ] ];
3179  }
3180 
3181  // Get the last edit not by this person...
3182  // Note: these may not be public values
3183  $actorWhere = ActorMigration::newMigration()->getWhere(
3184  $dbw,
3185  'rev_user',
3186  $current->getUser( RevisionRecord::RAW )
3187  );
3188 
3189  $s = $dbw->selectRow(
3190  [ 'revision' ] + $actorWhere['tables'],
3191  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3192  [
3193  'rev_page' => $current->getPageId(),
3194  'NOT(' . $actorWhere['conds'] . ')',
3195  ],
3196  __METHOD__,
3197  [
3198  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3199  'ORDER BY' => 'rev_timestamp DESC'
3200  ],
3201  $actorWhere['joins']
3202  );
3203  if ( $s === false ) {
3204  // No one else ever edited this page
3205  return [ [ 'cantrollback' ] ];
3206  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3207  || $s->rev_deleted & RevisionRecord::DELETED_USER
3208  ) {
3209  // Only admins can see this text
3210  return [ [ 'notvisiblerev' ] ];
3211  }
3212 
3213  // Generate the edit summary if necessary
3214  $target = $this->getRevisionStore()->getRevisionById(
3215  $s->rev_id,
3216  RevisionStore::READ_LATEST
3217  );
3218  if ( empty( $summary ) ) {
3219  if ( !$currentEditorForPublic ) { // no public user name
3220  $summary = wfMessage( 'revertpage-nouser' );
3221  } elseif ( $wgDisableAnonTalk && $current->getUser() === 0 ) {
3222  $summary = wfMessage( 'revertpage-anon' );
3223  } else {
3224  $summary = wfMessage( 'revertpage' );
3225  }
3226  }
3227  $legacyTarget = new Revision( $target );
3228  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3229 
3230  // Allow the custom summary to use the same args as the default message
3231  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3232  $args = [
3233  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3234  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3235  $s->rev_id,
3236  $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3237  $current->getId(),
3238  $contLang->timeanddate( $current->getTimestamp() )
3239  ];
3240  if ( $summary instanceof Message ) {
3241  $summary = $summary->params( $args )->inContentLanguage()->text();
3242  } else {
3243  $summary = wfMsgReplaceArgs( $summary, $args );
3244  }
3245 
3246  // Trim spaces on user supplied text
3247  $summary = trim( $summary );
3248 
3249  // Save
3250  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3251 
3252  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
3253  if ( $permissionManager->userHasRight( $guser, 'minoredit' ) ) {
3254  $flags |= EDIT_MINOR;
3255  }
3256 
3257  if ( $bot && ( $permissionManager->userHasAnyRight( $guser, 'markbotedits', 'bot' ) ) ) {
3258  $flags |= EDIT_FORCE_BOT;
3259  }
3260 
3261  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3262  $currentContent = $current->getContent( SlotRecord::MAIN );
3263  $targetContent = $target->getContent( SlotRecord::MAIN );
3264  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3265 
3266  if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3267  $tags[] = 'mw-rollback';
3268  }
3269 
3270  // Build rollback revision:
3271  // Restore old content
3272  // TODO: MCR: test this once we can store multiple slots
3273  foreach ( $target->getSlots()->getSlots() as $slot ) {
3274  $updater->inheritSlot( $slot );
3275  }
3276 
3277  // Remove extra slots
3278  // TODO: MCR: test this once we can store multiple slots
3279  foreach ( $current->getSlotRoles() as $role ) {
3280  if ( !$target->hasSlot( $role ) ) {
3281  $updater->removeSlot( $role );
3282  }
3283  }
3284 
3285  $updater->setOriginalRevisionId( $target->getId() );
3286  // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3287  $updater->addTags( $tags );
3288 
3289  // TODO: this logic should not be in the storage layer, it's here for compatibility
3290  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3291  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3292 
3293  if ( $wgUseRCPatrol && $permissionManager->userCan(
3294  'autopatrol', $guser, $this->getTitle()
3295  ) ) {
3296  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3297  }
3298 
3299  // Actually store the rollback
3300  $rev = $updater->saveRevision(
3302  $flags
3303  );
3304 
3305  // Set patrolling and bot flag on the edits, which gets rollbacked.
3306  // This is done even on edit failure to have patrolling in that case (T64157).
3307  $set = [];
3308  if ( $bot && $permissionManager->userHasRight( $guser, 'markbotedits' ) ) {
3309  // Mark all reverted edits as bot
3310  $set['rc_bot'] = 1;
3311  }
3312 
3313  if ( $wgUseRCPatrol ) {
3314  // Mark all reverted edits as patrolled
3315  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3316  }
3317 
3318  if ( count( $set ) ) {
3319  $actorWhere = ActorMigration::newMigration()->getWhere(
3320  $dbw,
3321  'rc_user',
3322  $current->getUser( RevisionRecord::RAW ),
3323  false
3324  );
3325  $dbw->update( 'recentchanges', $set,
3326  [ /* WHERE */
3327  'rc_cur_id' => $current->getPageId(),
3328  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3329  $actorWhere['conds'], // No tables/joins are needed for rc_user
3330  ],
3331  __METHOD__
3332  );
3333  }
3334 
3335  if ( !$updater->wasSuccessful() ) {
3336  return $updater->getStatus()->getErrorsArray();
3337  }
3338 
3339  // Report if the edit was not created because it did not change the content.
3340  if ( $updater->isUnchanged() ) {
3341  $resultDetails = [ 'current' => $legacyCurrent ];
3342  return [ [ 'alreadyrolled',
3343  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3344  htmlspecialchars( $fromP ),
3345  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3346  ] ];
3347  }
3348 
3349  if ( $changingContentModel ) {
3350  // If the content model changed during the rollback,
3351  // make sure it gets logged to Special:Log/contentmodel
3352  $log = new ManualLogEntry( 'contentmodel', 'change' );
3353  $log->setPerformer( $guser );
3354  $log->setTarget( $this->mTitle );
3355  $log->setComment( $summary );
3356  $log->setParameters( [
3357  '4::oldmodel' => $currentContent->getModel(),
3358  '5::newmodel' => $targetContent->getModel(),
3359  ] );
3360 
3361  $logId = $log->insert( $dbw );
3362  $log->publish( $logId );
3363  }
3364 
3365  $revId = $rev->getId();
3366 
3367  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3368 
3369  $resultDetails = [
3370  'summary' => $summary,
3371  'current' => $legacyCurrent,
3372  'target' => $legacyTarget,
3373  'newid' => $revId,
3374  'tags' => $tags
3375  ];
3376 
3377  // TODO: make this return a Status object and wrap $resultDetails in that.
3378  return [];
3379  }
3380 
3392  public static function onArticleCreate( Title $title ) {
3393  // TODO: move this into a PageEventEmitter service
3394 
3395  // Update existence markers on article/talk tabs...
3396  $other = $title->getOtherPage();
3397 
3398  $other->purgeSquid();
3399 
3400  $title->touchLinks();
3401  $title->purgeSquid();
3402  $title->deleteTitleProtection();
3403 
3404  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3405 
3406  // Invalidate caches of articles which include this page
3408  $title,
3409  'templatelinks',
3410  [ 'causeAction' => 'page-create' ]
3411  );
3412  JobQueueGroup::singleton()->lazyPush( $job );
3413 
3414  if ( $title->getNamespace() == NS_CATEGORY ) {
3415  // Load the Category object, which will schedule a job to create
3416  // the category table row if necessary. Checking a replica DB is ok
3417  // here, in the worst case it'll run an unnecessary recount job on
3418  // a category that probably doesn't have many members.
3419  Category::newFromTitle( $title )->getID();
3420  }
3421  }
3422 
3428  public static function onArticleDelete( Title $title ) {
3429  // TODO: move this into a PageEventEmitter service
3430 
3431  // Update existence markers on article/talk tabs...
3432  // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3433  BacklinkCache::get( $title )->clear();
3434  $other = $title->getOtherPage();
3435 
3436  $other->purgeSquid();
3437 
3438  $title->touchLinks();
3439  $title->purgeSquid();
3440 
3441  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3442 
3443  // File cache
3446 
3447  // Messages
3448  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3449  MessageCache::singleton()->updateMessageOverride( $title, null );
3450  }
3451 
3452  // Images
3453  if ( $title->getNamespace() == NS_FILE ) {
3455  $title,
3456  'imagelinks',
3457  [ 'causeAction' => 'page-delete' ]
3458  );
3459  JobQueueGroup::singleton()->lazyPush( $job );
3460  }
3461 
3462  // User talk pages
3463  if ( $title->getNamespace() == NS_USER_TALK ) {
3464  $user = User::newFromName( $title->getText(), false );
3465  if ( $user ) {
3466  $user->setNewtalk( false );
3467  }
3468  }
3469 
3470  // Image redirects
3471  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3472 
3473  // Purge cross-wiki cache entities referencing this page
3475  }
3476 
3485  public static function onArticleEdit(
3486  Title $title,
3487  Revision $revision = null,
3488  $slotsChanged = null
3489  ) {
3490  // TODO: move this into a PageEventEmitter service
3491  $jobs = [];
3492  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3493  // Invalidate caches of articles which include this page.
3494  // Only for the main slot, because only the main slot is transcluded.
3495  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3497  $title,
3498  'templatelinks',
3499  [ 'causeAction' => 'page-edit' ]
3500  );
3501  }
3502  // Invalidate the caches of all pages which redirect here
3504  $title,
3505  'redirect',
3506  [ 'causeAction' => 'page-edit' ]
3507  );
3508  JobQueueGroup::singleton()->lazyPush( $jobs );
3509 
3510  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3511 
3512  // Purge CDN for this page only
3513  $title->purgeSquid();
3514  // Clear file cache for this page only
3516 
3517  // Purge ?action=info cache
3518  $revid = $revision ? $revision->getId() : null;
3519  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3521  } );
3522 
3523  // Purge cross-wiki cache entities referencing this page
3525  }
3526 
3534  private static function purgeInterwikiCheckKey( Title $title ) {
3536 
3537  if ( !$wgEnableScaryTranscluding ) {
3538  return; // @todo: perhaps this wiki is only used as a *source* for content?
3539  }
3540 
3541  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3542  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3543  $cache->resetCheckKey(
3544  // Do not include the namespace since there can be multiple aliases to it
3545  // due to different namespace text definitions on different wikis. This only
3546  // means that some cache invalidations happen that are not strictly needed.
3547  $cache->makeGlobalKey(
3548  'interwiki-page',
3550  $title->getDBkey()
3551  )
3552  );
3553  } );
3554  }
3555 
3562  public function getCategories() {
3563  $id = $this->getId();
3564  if ( $id == 0 ) {
3565  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3566  }
3567 
3568  $dbr = wfGetDB( DB_REPLICA );
3569  $res = $dbr->select( 'categorylinks',
3570  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3571  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3572  // as not being aliases, and NS_CATEGORY is numeric
3573  [ 'cl_from' => $id ],
3574  __METHOD__ );
3575 
3576  return TitleArray::newFromResult( $res );
3577  }
3578 
3585  public function getHiddenCategories() {
3586  $result = [];
3587  $id = $this->getId();
3588 
3589  if ( $id == 0 ) {
3590  return [];
3591  }
3592 
3593  $dbr = wfGetDB( DB_REPLICA );
3594  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3595  [ 'cl_to' ],
3596  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3597  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3598  __METHOD__ );
3599 
3600  if ( $res !== false ) {
3601  foreach ( $res as $row ) {
3602  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3603  }
3604  }
3605 
3606  return $result;
3607  }
3608 
3616  public function getAutoDeleteReason( &$hasHistory ) {
3617  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3618  }
3619 
3630  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3631  $id = $id ?: $this->getId();
3632  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3633  getCategoryLinkType( $this->getTitle()->getNamespace() );
3634 
3635  $addFields = [ 'cat_pages = cat_pages + 1' ];
3636  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3637  if ( $type !== 'page' ) {
3638  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3639  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3640  }
3641 
3642  $dbw = wfGetDB( DB_MASTER );
3643 
3644  if ( count( $added ) ) {
3645  $existingAdded = $dbw->selectFieldValues(
3646  'category',
3647  'cat_title',
3648  [ 'cat_title' => $added ],
3649  __METHOD__
3650  );
3651 
3652  // For category rows that already exist, do a plain
3653  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3654  // to avoid creating gaps in the cat_id sequence.
3655  if ( count( $existingAdded ) ) {
3656  $dbw->update(
3657  'category',
3658  $addFields,
3659  [ 'cat_title' => $existingAdded ],
3660  __METHOD__
3661  );
3662  }
3663 
3664  $missingAdded = array_diff( $added, $existingAdded );
3665  if ( count( $missingAdded ) ) {
3666  $insertRows = [];
3667  foreach ( $missingAdded as $cat ) {
3668  $insertRows[] = [
3669  'cat_title' => $cat,
3670  'cat_pages' => 1,
3671  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3672  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3673  ];
3674  }
3675  $dbw->upsert(
3676  'category',
3677  $insertRows,
3678  [ 'cat_title' ],
3679  $addFields,
3680  __METHOD__
3681  );
3682  }
3683  }
3684 
3685  if ( count( $deleted ) ) {
3686  $dbw->update(
3687  'category',
3688  $removeFields,
3689  [ 'cat_title' => $deleted ],
3690  __METHOD__
3691  );
3692  }
3693 
3694  foreach ( $added as $catName ) {
3695  $cat = Category::newFromName( $catName );
3696  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3697  }
3698 
3699  foreach ( $deleted as $catName ) {
3700  $cat = Category::newFromName( $catName );
3701  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3702  // Refresh counts on categories that should be empty now (after commit, T166757)
3703  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3704  $cat->refreshCountsIfEmpty();
3705  } );
3706  }
3707  }
3708 
3715  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3716  if ( wfReadOnly() ) {
3717  return;
3718  }
3719 
3720  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3721  [ $this, $this->mTitle, $parserOutput ]
3722  ) ) {
3723  return;
3724  }
3725 
3726  $config = RequestContext::getMain()->getConfig();
3727 
3728  $params = [
3729  'isOpportunistic' => true,
3730  'rootJobTimestamp' => $parserOutput->getCacheTime()
3731  ];
3732 
3733  if ( $this->mTitle->areRestrictionsCascading() ) {
3734  // If the page is cascade protecting, the links should really be up-to-date
3735  JobQueueGroup::singleton()->lazyPush(
3736  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3737  );
3738  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3739  // Assume the output contains "dynamic" time/random based magic words.
3740  // Only update pages that expired due to dynamic content and NOT due to edits
3741  // to referenced templates/files. When the cache expires due to dynamic content,
3742  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3743  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3744  // template/file edit already triggered recursive RefreshLinksJob jobs.
3745  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3746  // If a page is uncacheable, do not keep spamming a job for it.
3747  // Although it would be de-duplicated, it would still waste I/O.
3749  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3750  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3751  if ( $cache->add( $key, time(), $ttl ) ) {
3752  JobQueueGroup::singleton()->lazyPush(
3753  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3754  );
3755  }
3756  }
3757  }
3758  }
3759 
3769  public function getDeletionUpdates( $rev = null ) {
3770  if ( !$rev ) {
3771  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3772 
3773  try {
3774  $rev = $this->getRevisionRecord();
3775  } catch ( Exception $ex ) {
3776  // If we can't load the content, something is wrong. Perhaps that's why
3777  // the user is trying to delete the page, so let's not fail in that case.
3778  // Note that doDeleteArticleReal() will already have logged an issue with
3779  // loading the content.
3780  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3781  }
3782  }
3783 
3784  if ( !$rev ) {
3785  $slotContent = [];
3786  } elseif ( $rev instanceof Content ) {
3787  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3788 
3789  $slotContent = [ SlotRecord::MAIN => $rev ];
3790  } else {
3791  $slotContent = array_map( function ( SlotRecord $slot ) {
3792  return $slot->getContent( RevisionRecord::RAW );
3793  }, $rev->getSlots()->getSlots() );
3794  }
3795 
3796  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3797 
3798  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3799  // model here, not the content object!
3800  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3802  foreach ( $slotContent as $role => $content ) {
3803  $handler = $content->getContentHandler();
3804 
3805  $updates = $handler->getDeletionUpdates(
3806  $this->getTitle(),
3807  $role
3808  );
3809  $allUpdates = array_merge( $allUpdates, $updates );
3810 
3811  // TODO: remove B/C hack in 1.32!
3812  $legacyUpdates = $content->getDeletionUpdates( $this );
3813 
3814  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3815  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3816  return !( $update instanceof LinksDeletionUpdate );
3817  } );
3818 
3819  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3820  }
3821 
3822  Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3823 
3824  // TODO: hard deprecate old hook in 1.33
3825  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3826  return $allUpdates;
3827  }
3828 
3836  public function isLocal() {
3837  return true;
3838  }
3839 
3849  public function getWikiDisplayName() {
3850  global $wgSitename;
3851  return $wgSitename;
3852  }
3853 
3862  public function getSourceURL() {
3863  return $this->getTitle()->getCanonicalURL();
3864  }
3865 
3872  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3873 
3874  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3875  }
3876 
3877 }
WikiPage\getCategories
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:3562
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:42
$wgUseAutomaticEditSummaries
$wgUseAutomaticEditSummaries
If user doesn't specify any edit summary when making a an edit, MediaWiki will try to automatically c...
Definition: DefaultSettings.php:6703
Revision\getTimestamp
getTimestamp()
Definition: Revision.php:798
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:254
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
Page
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:29
WikiPage\getComment
getComment( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:915
CacheTime\getCacheExpiry
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:129
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:539
Revision\newKnownCurrent
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:1129
WikiPage\onArticleCreate
static onArticleCreate(Title $title)
The onArticle*() functions are supposed to be a kind of hooks which should be called whenever any of ...
Definition: WikiPage.php:3392
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
WikiPage\loadPageData
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:489
RepoGroup\singleton
static singleton()
Definition: RepoGroup.php:60
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:292
WikiPage\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3616
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:799
ParserOutput
Definition: ParserOutput.php:25
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
WikiPage\getRedirectTarget
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:999
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:342
User\getId
getId()
Get the user's ID.
Definition: User.php:2203
Revision\newFromId
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:119
WikiPage\clearCacheFields
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:317
Title\getFragment
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1707
WikiPage\isBatchedDelete
isBatchedDelete( $safetyMargin=0)
Determines if deletion of this page would be batched (executed over time by the job queue) or not (co...
Definition: WikiPage.php:2575
$wgPageCreationLog
$wgPageCreationLog
Maintain a log of page creations at Special:Log/create?
Definition: DefaultSettings.php:7923
WikiPage\updateRevisionOn
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1384
WikiPage\wasLoadedFrom
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:534
TitleArray\newFromResult
static newFromResult( $res)
Definition: TitleArray.php:42
WikiPage\getUndoContent
getUndoContent(Revision $undo, Revision $undoafter)
Get the content that needs to be saved in order to undo all revisions between $undo and $undoafter.
Definition: WikiPage.php:1556
HTMLFileCache\clearFileCache
static clearFileCache(Title $title)
Clear the file caches for a page for all actions.
Definition: HTMLFileCache.php:223
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:136
WikiPage\getCreator
getCreator( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:877
EDIT_INTERNAL
const EDIT_INTERNAL
Definition: Defines.php:139
MediaWiki\Storage\DerivedPageDataUpdater\setArticleCountMethod
setArticleCountMethod( $articleCountMethod)
Definition: DerivedPageDataUpdater.php:420
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:79
WikiPage\newPageUpdater
newPageUpdater(User $user, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
Definition: WikiPage.php:1801
WikiPage\hasViewableContent
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition: WikiPage.php:625
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:692
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1869
WikiPage\doViewUpdates
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1274
MediaWiki\Storage\DerivedPageDataUpdater\setRcWatchCategoryMembership
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
Definition: DerivedPageDataUpdater.php:428
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1635
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1681
WikiPage\$mDataLoadedFrom
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:86
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Definition: DeferredUpdates.php:85
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:47
WikiPage\replaceSectionContent
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1596
PoolWorkArticleView
Definition: PoolWorkArticleView.php:28
NS_FILE
const NS_FILE
Definition: Defines.php:66
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1961
$wgMultiContentRevisionSchemaMigrationStage
int $wgMultiContentRevisionSchemaMigrationStage
RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables).
Definition: DefaultSettings.php:9003
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1123
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1171
wfMsgReplaceArgs
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
Definition: GlobalFunctions.php:1299
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:515
WikiPage\getRevision
getRevision()
Get the latest revision.
Definition: WikiPage.php:787
RefreshLinksJob\newDynamic
static newDynamic(Title $title, array $params)
Definition: RefreshLinksJob.php:80
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1264
RefreshLinksJob\newPrioritized
static newPrioritized(Title $title, array $params)
Definition: RefreshLinksJob.php:68
Revision\getContentHandler
getContentHandler()
Returns the content handler appropriate for this revision's content model.
Definition: Revision.php:791
$s
$s
Definition: mergeMessageFileList.php:185
WikiPage\getContributors
getContributors()
Get a list of users who have edited this article, not including the user who made the most recent rev...
Definition: WikiPage.php:1161
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:571
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1078
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
$success
$success
Definition: NoLocalSettings.php:42
Revision\newFromPageId
static newFromPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
Definition: Revision.php:157
User\pingLimiter
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1911
Message
$res
$res
Definition: testCompression.php:52
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
Definition: FakeResultWrapper.php:11
$wgContentHandlerUseDB
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility.
Definition: DefaultSettings.php:8642
WikiPage\archiveRevisions
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:2842
WikiPage\getSlotRoleRegistry
getSlotRoleRegistry()
Definition: WikiPage.php:253
WikiPage\onArticleEdit
static onArticleEdit(Title $title, Revision $revision=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3485
WikiPage\getDBLoadBalancer
getDBLoadBalancer()
Definition: WikiPage.php:267
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:277
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:6927
$revQuery
$revQuery
Definition: testCompression.php:51
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:6943
WikiPage\flattenRestrictions
static flattenRestrictions( $limit)
Take an array of page restrictions and flatten it to a string suitable for insertion into the page_re...
Definition: WikiPage.php:2548
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:136
HTMLCacheUpdateJob\newForBacklinks
static newForBacklinks(Title $title, $table, $params=[])
Definition: HTMLCacheUpdateJob.php:59
WikiPage\$mTitle
Title $mTitle
Definition: WikiPage.php:53
WikiPage\$mTouched
string $mTouched
Definition: WikiPage.php:106
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
WikiPage\getUserText
getUserText( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:896
WikiPage\triggerOpportunisticLinksUpdate
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3715
WikiPage\protectDescription
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2483
$dbr
$dbr
Definition: testCompression.php:50
IDBAccessObject\READ_LOCKING
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:64
Revision
Definition: Revision.php:40
User\matchEditToken
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4396
WikiPage\updateParserCache
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2088
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1578
$wgEnableScaryTranscluding
$wgEnableScaryTranscluding
Enable interwiki transcluding.
Definition: DefaultSettings.php:4334
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:1013
Revision\getQueryInfo
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object.
Definition: Revision.php:315
WikiCategoryPage
Special handling for category pages.
Definition: WikiCategoryPage.php:26
MWException
MediaWiki exception.
Definition: MWException.php:26
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
WikiPage\doDeleteArticleBatched
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2675
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:929
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Definition: GlobalFunctions.php:1044
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
WikiPage\doRollback
doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags=null)
Roll back the most recent consecutive set of edits to a page from the same user; fails if there are n...
Definition: WikiPage.php:3099
MediaWiki\Storage\DerivedPageDataUpdater\setLogger
setLogger(LoggerInterface $logger)
Definition: DerivedPageDataUpdater.php:307
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1037
wfArrayDiff2
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
Definition: GlobalFunctions.php:113
BacklinkCache\get
static get(Title $title)
Create a new BacklinkCache or reuse any existing one.
Definition: BacklinkCache.php:113
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:518
wfIncrStats
wfIncrStats( $key, $count=1)
Increment a statistics counter.
Definition: GlobalFunctions.php:1161
WikiPage\doSecondaryDataUpdates
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2132
WikiPage\selectFields
static selectFields()
Return the list of revision fields that should be selected to create a new page.
Definition: WikiPage.php:348
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2575
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:337
Title\getInterwiki
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:923
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1233
WikiPage\getId
getId()
Definition: WikiPage.php:600
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1339
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1211
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:298
wfTimestampOrNull
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Definition: GlobalFunctions.php:1885
WikiPage\exists
exists()
Definition: WikiPage.php:610
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:130
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3428
ObjectCache\getInstance
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:80
WikiPage\$mRedirectTarget
Title $mRedirectTarget
Definition: WikiPage.php:91
WikiPage\__construct
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:122
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:681
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:70
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:703
WikiPage\purgeInterwikiCheckKey
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3534
WikiPage\$mDataLoaded
bool $mDataLoaded
Definition: WikiPage.php:59
ChangeTags\getSoftwareTags
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:58
$title
$title
Definition: testCompression.php:34
SiteStatsUpdate\factory
static factory(array $deltas)
Definition: SiteStatsUpdate.php:71
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
WikiPage\setTimestamp
setTimestamp( $ts)
Set the page timestamp (use only to avoid DB queries)
Definition: WikiPage.php:845
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:74
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:714
ParserOptions\getStubThreshold
getStubThreshold()
Thumb size preferred by the user.
Definition: ParserOptions.php:565
User\newFromAnyId
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:596
DB_MASTER
const DB_MASTER
Definition: defines.php:26
IDBAccessObject\READ_NONE
const READ_NONE
Definition: IDBAccessObject.php:73
WikiPage\$mLatest
int false $mLatest
False means "not loaded".
Definition: WikiPage.php:71
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:913
WikiPage\doPurge
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1297
WikiPage\isCountable
isCountable( $editInfo=false)
Determine whether a page would be suitable for being counted as an article in the site_stats table ba...
Definition: WikiPage.php:946
MessageCache\singleton
static singleton()
Get the singleton instance of this class.
Definition: MessageCache.php:114
User\clearNotification
clearNotification(&$title, $oldid=0)
Clear the user's notification timestamp for the given title.
Definition: User.php:3696
WikiPage\getContentModel
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:652
WikiPage\pageDataFromTitle
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object's namespace and title using a sanitized title string.
Definition: WikiPage.php:459
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2988
Wikimedia\Rdbms\LoadBalancer
Database connection, tracking, load balancing, and transaction manager for a cluster.
Definition: LoadBalancer.php:42
$wgPageLanguageUseDB
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
Definition: DefaultSettings.php:8730
Category\newFromTitle
static newFromTitle( $title)
Factory function.
Definition: Category.php:146
WikiPage\getDerivedDataUpdater
getDerivedDataUpdater(User $forUser=null, RevisionRecord $forRevision=null, RevisionSlotsUpdate $forUpdate=null, $forEdit=false)
Returns a DerivedPageDataUpdater for use with the given target revision or new content.
Definition: WikiPage.php:1745
WikiPage\updateIfNewerOn
updateIfNewerOn( $dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record.
Definition: WikiPage.php:1499
WikiPage\setLastEdit
setLastEdit(Revision $revision)
Set the latest revision.
Definition: WikiPage.php:778
MediaWiki\Storage\RevisionSlotsUpdate
Value object representing a modification of revision slots.
Definition: RevisionSlotsUpdate.php:36
Revision\RevisionRenderer
The RevisionRenderer service provides access to rendered output for revisions.
Definition: RevisionRenderer.php:45
$content
$content
Definition: router.php:78
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:63
WikiPage\getDeletionUpdates
getDeletionUpdates( $rev=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3769
WikiPage\protectDescriptionLog
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2525
WikiPage\insertRedirect
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1046
WikiPage\doDeleteArticleReal
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,...
Definition: WikiPage.php:2637
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:133
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:48
CdnCacheUpdate
Handles purging the appropriate CDN objects given a list of URLs or Title instances.
Definition: CdnCacheUpdate.php:30
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
MediaWiki\Storage\PageUpdater
Controller-like object for creating and updating pages by creating new revisions.
Definition: PageUpdater.php:72
SCHEMA_COMPAT_WRITE_OLD
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:264
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:116
WikiPage\newFromID
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:180
WikiPage\insertProtectNullRevision
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2418
$wgSitename
$wgSitename
Name of the site.
Definition: DefaultSettings.php:80
WikiPage\getSourceURL
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3862
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(Title $title, Revision $old=null, Revision $new=null, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:533
ParserOptions\newCanonical
static newCanonical( $context=null, $userLang=null)
Creates a "canonical" ParserOptions object.
Definition: ParserOptions.php:1073
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1551
WikiPage\$derivedDataUpdater
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:116
LinksDeletionUpdate
Update object handling the cleanup of links tables after a page was deleted.
Definition: LinksDeletionUpdate.php:28
WikiPage\commitRollback
commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser, $tags=null)
Backend implementation of doRollback(), please refer there for parameter and return value documentati...
Definition: WikiPage.php:3145
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3585
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:210
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:81
$wgDeleteRevisionsBatchSize
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
Definition: DefaultSettings.php:5539
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:431
IP\isValid
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
WikiPage\doEditContent
doEditContent(Content $content, $summary, $flags=0, $originalRevId=false, User $user=null, $serialFormat=null, $tags=[], $undidRevId=0)
Change an existing article or create a new article.
Definition: WikiPage.php:1882
WikiPage\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object.
Definition: WikiPage.php:387
WikiPage\getOldestRevision
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:725
WikiPage\loadLastEdit
loadLastEdit()
Loads everything except the text This isn't necessary for all uses, so it's only done if needed.
Definition: WikiPage.php:738
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1112
$context
$context
Definition: load.php:45
Content
Base interface for content objects.
Definition: Content.php:34
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:132
WikiPage\loadFromRow
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:560
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:5333
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2461
$args
if( $line===false) $args
Definition: cdb.php:64
Title
Represents a title within MediaWiki.
Definition: Title.php:42
$status
return $status
Definition: SyntaxHighlight.php:347
WikiPage\getUser
getUser( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:858
wfRandom
wfRandom()
Get a random decimal value in the domain of [0, 1), in a way not likely to give duplicate values for ...
Definition: GlobalFunctions.php:256
WikiPage\$mIsRedirect
bool $mIsRedirect
Definition: WikiPage.php:65
WikiPage\newDerivedDataUpdater
newDerivedDataUpdater()
Definition: WikiPage.php:1696
JobQueueGroup\singleton
static singleton( $domain=false)
Definition: JobQueueGroup.php:70
wfReadOnlyReason
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
Definition: GlobalFunctions.php:1184
$cache
$cache
Definition: mcc.php:33
WikiPage\doEditUpdates
doEditUpdates(Revision $revision, User $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2060
DeferredUpdates\PRESEND
const PRESEND
Definition: DeferredUpdates.php:69
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:50
WebRequest\getRequestId
static getRequestId()
Get the unique request ID.
Definition: WebRequest.php:303
$wgAjaxEditStash
$wgAjaxEditStash
Have clients send edits to be prepared when filling in edit summaries.
Definition: DefaultSettings.php:8242
WikiPage\$mId
int $mId
Definition: WikiPage.php:81
DeletePageJob
Class DeletePageJob.
Definition: DeletePageJob.php:6
WikiPage\getWikiDisplayName
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3849
LinksUpdate\queueRecursiveJobsForTable
static queueRecursiveJobsForTable(Title $title, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
Definition: LinksUpdate.php:366
WikiPage\prepareContentForEdit
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:1991
Revision\loadFromTimestamp
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:296
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:222
WikiPage\updateCategoryCounts
updateCategoryCounts(array $added, array $deleted, $id=0)
Update all the appropriate counts in the category table, given that we've added the categories $added...
Definition: WikiPage.php:3630
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3871
InfoAction\invalidateCache
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:71
$source
$source
Definition: mwdoc-filter.php:34
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:37
WikiPage\pageData
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:426
Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
Revision\newNullRevision
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
Definition: Revision.php:1000
MediaWiki\Edit\PreparedEdit
Represents information returned by WikiPage::prepareContentForEdit()
Definition: PreparedEdit.php:35
WikiFilePage
Special handling for file pages.
Definition: WikiFilePage.php:31
WikiPage\isLocal
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3836
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1065
$wgArticleCountMethod
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article.
Definition: DefaultSettings.php:4377
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:68
Category\newFromName
static newFromName( $name)
Factory function.
Definition: Category.php:126
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:134
ParserOptions\isSafeToCache
isSafeToCache()
Test whether these options are safe to cache.
Definition: ParserOptions.php:1397
WikiPage\insertRedirectEntry
insertRedirectEntry(Title $rt, $oldLatest=null)
Insert or update the redirect table entry for this page to indicate it redirects to $rt.
Definition: WikiPage.php:1072
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:831
WikiPage\getRevisionRenderer
getRevisionRenderer()
Definition: WikiPage.php:246
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1463
WikiPage\$mPreparedEdit
PreparedEdit false $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:76
Revision\getRevisionRecord
getRevisionRecord()
Definition: Revision.php:433
WikiPage\$mLinksUpdated
string $mLinksUpdated
Definition: WikiPage.php:111
WikiPage\doDeleteArticle
doDeleteArticle( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $user=null, $immediate=false)
Same as doDeleteArticleReal(), but returns a simple boolean.
Definition: WikiPage.php:2604
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:634
WikiPage\$mLastRevision
Revision $mLastRevision
Definition: WikiPage.php:96
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:100
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:60
CommentStore\getStore
static getStore()
Definition: CommentStore.php:139
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:124
ParserOutput\hasDynamicContent
hasDynamicContent()
Check whether the cache TTL was lowered due to dynamic content.
Definition: ParserOutput.php:1275
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
WikiPage\getParserCache
getParserCache()
Definition: WikiPage.php:260
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:473
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:290
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:820
WikiPage\hasDifferencesOutsideMainSlot
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
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2232
CommentStoreComment
CommentStoreComment represents a comment stored by CommentStore.
Definition: CommentStoreComment.php:29
WikiPage\$mTimestamp
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition: WikiPage.php:101
Title\purgeExpiredRestrictions
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:2974
$wgDisableAnonTalk
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
Definition: DefaultSettings.php:7040
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
$wgRCWatchCategoryMembership
$wgRCWatchCategoryMembership
Treat category membership changes as a RecentChange.
Definition: DefaultSettings.php:6917
WikiPage\doDeleteUpdates
doDeleteUpdates( $id, Content $content=null, Revision $revision=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:3016
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:306
WikiPage\doUpdateRestrictions
doUpdateRestrictions(array $limit, array $expiry, &$cascade, $reason, User $user, $tags=null)
Update the article's restriction field, and leave a log entry.
Definition: WikiPage.php:2162
WikiPage\getRevisionStore
getRevisionStore()
Definition: WikiPage.php:239
$type
$type
Definition: testCompression.php:48