MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
36 use Wikimedia\Assert\Assert;
37 use Wikimedia\IPUtils;
41 
48 class WikiPage implements Page, IDBAccessObject {
49  // Constants for $mDataLoadedFrom and related
50 
54  public $mTitle = null;
55 
60  public $mDataLoaded = false;
61 
66  public $mIsRedirect = false;
67 
72  public $mLatest = false;
73 
77  public $mPreparedEdit = false;
78 
82  protected $mId = null;
83 
88 
92  protected $mRedirectTarget = null;
93 
97  protected $mLastRevision = null;
98 
102  protected $mTimestamp = '';
103 
107  protected $mTouched = '19700101000000';
108 
112  protected $mLinksUpdated = '19700101000000';
113 
117  private $derivedDataUpdater = null;
118 
123  public function __construct( Title $title ) {
124  $this->mTitle = $title;
125  }
126 
131  public function __clone() {
132  $this->mTitle = clone $this->mTitle;
133  }
134 
143  public static function factory( Title $title ) {
144  $ns = $title->getNamespace();
145 
146  if ( $ns == NS_MEDIA ) {
147  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
148  } elseif ( $ns < 0 ) {
149  throw new MWException( "Invalid or virtual namespace $ns given." );
150  }
151 
152  $page = null;
153  if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
154  return $page;
155  }
156 
157  switch ( $ns ) {
158  case NS_FILE:
159  $page = new WikiFilePage( $title );
160  break;
161  case NS_CATEGORY:
162  $page = new WikiCategoryPage( $title );
163  break;
164  default:
165  $page = new WikiPage( $title );
166  }
167 
168  return $page;
169  }
170 
181  public static function newFromID( $id, $from = 'fromdb' ) {
182  // page ids are never 0 or negative, see T63166
183  if ( $id < 1 ) {
184  return null;
185  }
186 
187  $from = self::convertSelectType( $from );
188  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
189  $pageQuery = self::getQueryInfo();
190  $row = $db->selectRow(
191  $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
192  [], $pageQuery['joins']
193  );
194  if ( !$row ) {
195  return null;
196  }
197  return self::newFromRow( $row, $from );
198  }
199 
211  public static function newFromRow( $row, $from = 'fromdb' ) {
212  $page = self::factory( Title::newFromRow( $row ) );
213  $page->loadFromRow( $row, $from );
214  return $page;
215  }
216 
223  protected static function convertSelectType( $type ) {
224  switch ( $type ) {
225  case 'fromdb':
226  return self::READ_NORMAL;
227  case 'fromdbmaster':
228  return self::READ_LATEST;
229  case 'forupdate':
230  return self::READ_LOCKING;
231  default:
232  // It may already be an integer or whatever else
233  return $type;
234  }
235  }
236 
240  private function getRevisionStore() {
241  return MediaWikiServices::getInstance()->getRevisionStore();
242  }
243 
247  private function getRevisionRenderer() {
248  return MediaWikiServices::getInstance()->getRevisionRenderer();
249  }
250 
254  private function getSlotRoleRegistry() {
255  return MediaWikiServices::getInstance()->getSlotRoleRegistry();
256  }
257 
262  return MediaWikiServices::getInstance()->getContentHandlerFactory();
263  }
264 
268  private function getParserCache() {
269  return MediaWikiServices::getInstance()->getParserCache();
270  }
271 
275  private function getDBLoadBalancer() {
276  return MediaWikiServices::getInstance()->getDBLoadBalancer();
277  }
278 
285  public function getActionOverrides() {
286  return $this->getContentHandler()->getActionOverrides();
287  }
288 
298  public function getContentHandler() {
299  return $this->getContentHandlerFactory()
300  ->getContentHandler( $this->getContentModel() );
301  }
302 
307  public function getTitle() {
308  return $this->mTitle;
309  }
310 
315  public function clear() {
316  $this->mDataLoaded = false;
317  $this->mDataLoadedFrom = self::READ_NONE;
318 
319  $this->clearCacheFields();
320  }
321 
326  protected function clearCacheFields() {
327  $this->mId = null;
328  $this->mRedirectTarget = null; // Title object if set
329  $this->mLastRevision = null; // Latest revision
330  $this->mTouched = '19700101000000';
331  $this->mLinksUpdated = '19700101000000';
332  $this->mTimestamp = '';
333  $this->mIsRedirect = false;
334  $this->mLatest = false;
335  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
336  // checks the requested rev ID and content against the cached one. For most
337  // content types, the output should not change during the lifetime of this cache.
338  // Clearing it can cause extra parses on edit for no reason.
339  }
340 
346  public function clearPreparedEdit() {
347  $this->mPreparedEdit = false;
348  }
349 
359  public static function getQueryInfo() {
360  global $wgPageLanguageUseDB;
361 
362  $ret = [
363  'tables' => [ 'page' ],
364  'fields' => [
365  'page_id',
366  'page_namespace',
367  'page_title',
368  'page_restrictions',
369  'page_is_redirect',
370  'page_is_new',
371  'page_random',
372  'page_touched',
373  'page_links_updated',
374  'page_latest',
375  'page_len',
376  'page_content_model',
377  ],
378  'joins' => [],
379  ];
380 
381  if ( $wgPageLanguageUseDB ) {
382  $ret['fields'][] = 'page_lang';
383  }
384 
385  return $ret;
386  }
387 
395  protected function pageData( $dbr, $conditions, $options = [] ) {
396  $pageQuery = self::getQueryInfo();
397 
398  // Avoid PHP 7.1 warning of passing $this by reference
399  $wikiPage = $this;
400 
401  Hooks::run( 'ArticlePageDataBefore', [
402  &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
403  ] );
404 
405  $row = $dbr->selectRow(
406  $pageQuery['tables'],
407  $pageQuery['fields'],
408  $conditions,
409  __METHOD__,
410  $options,
411  $pageQuery['joins']
412  );
413 
414  Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
415 
416  return $row;
417  }
418 
428  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
429  return $this->pageData( $dbr, [
430  'page_namespace' => $title->getNamespace(),
431  'page_title' => $title->getDBkey() ], $options );
432  }
433 
442  public function pageDataFromId( $dbr, $id, $options = [] ) {
443  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
444  }
445 
458  public function loadPageData( $from = 'fromdb' ) {
459  $from = self::convertSelectType( $from );
460  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
461  // We already have the data from the correct location, no need to load it twice.
462  return;
463  }
464 
465  if ( is_int( $from ) ) {
466  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
467  $loadBalancer = $this->getDBLoadBalancer();
468  $db = $loadBalancer->getConnection( $index );
469  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
470 
471  if ( !$data
472  && $index == DB_REPLICA
473  && $loadBalancer->getServerCount() > 1
474  && $loadBalancer->hasOrMadeRecentMasterChanges()
475  ) {
476  $from = self::READ_LATEST;
477  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
478  $db = $loadBalancer->getConnection( $index );
479  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
480  }
481  } else {
482  // No idea from where the caller got this data, assume replica DB.
483  $data = $from;
484  $from = self::READ_NORMAL;
485  }
486 
487  $this->loadFromRow( $data, $from );
488  }
489 
503  public function wasLoadedFrom( $from ) {
504  $from = self::convertSelectType( $from );
505 
506  if ( !is_int( $from ) ) {
507  // No idea from where the caller got this data, assume replica DB.
508  $from = self::READ_NORMAL;
509  }
510 
511  if ( $from <= $this->mDataLoadedFrom ) {
512  return true;
513  }
514 
515  return false;
516  }
517 
529  public function loadFromRow( $data, $from ) {
530  $lc = MediaWikiServices::getInstance()->getLinkCache();
531  $lc->clearLink( $this->mTitle );
532 
533  if ( $data ) {
534  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
535 
536  $this->mTitle->loadFromRow( $data );
537 
538  // Old-fashioned restrictions
539  $this->mTitle->loadRestrictions( $data->page_restrictions );
540 
541  $this->mId = intval( $data->page_id );
542  $this->mTouched = MWTimestamp::convert( TS_MW, $data->page_touched );
543  $this->mLinksUpdated = $data->page_links_updated === null
544  ? null
545  : MWTimestamp::convert( TS_MW, $data->page_links_updated );
546  $this->mIsRedirect = intval( $data->page_is_redirect );
547  $this->mLatest = intval( $data->page_latest );
548  // T39225: $latest may no longer match the cached latest Revision object.
549  // Double-check the ID of any cached latest Revision object for consistency.
550  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
551  $this->mLastRevision = null;
552  $this->mTimestamp = '';
553  }
554  } else {
555  $lc->addBadLinkObj( $this->mTitle );
556 
557  $this->mTitle->loadFromRow( false );
558 
559  $this->clearCacheFields();
560 
561  $this->mId = 0;
562  }
563 
564  $this->mDataLoaded = true;
565  $this->mDataLoadedFrom = self::convertSelectType( $from );
566  }
567 
571  public function getId() {
572  if ( !$this->mDataLoaded ) {
573  $this->loadPageData();
574  }
575  return $this->mId;
576  }
577 
581  public function exists() {
582  if ( !$this->mDataLoaded ) {
583  $this->loadPageData();
584  }
585  return $this->mId > 0;
586  }
587 
596  public function hasViewableContent() {
597  return $this->mTitle->isKnown();
598  }
599 
605  public function isRedirect() {
606  if ( !$this->mDataLoaded ) {
607  $this->loadPageData();
608  }
609 
610  return (bool)$this->mIsRedirect;
611  }
612 
623  public function getContentModel() {
624  if ( $this->exists() ) {
625  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
626 
627  return $cache->getWithSetCallback(
628  $cache->makeKey( 'page-content-model', $this->getLatest() ),
629  $cache::TTL_MONTH,
630  function () {
631  $rev = $this->getRevision();
632  if ( $rev ) {
633  // Look at the revision's actual content model
634  return $rev->getContentModel();
635  } else {
636  $title = $this->mTitle->getPrefixedDBkey();
637  wfWarn( "Page $title exists but has no (visible) revisions!" );
638  return $this->mTitle->getContentModel();
639  }
640  }
641  );
642  }
643 
644  // use the default model for this page
645  return $this->mTitle->getContentModel();
646  }
647 
652  public function checkTouched() {
653  if ( !$this->mDataLoaded ) {
654  $this->loadPageData();
655  }
656  return ( $this->mId && !$this->mIsRedirect );
657  }
658 
663  public function getTouched() {
664  if ( !$this->mDataLoaded ) {
665  $this->loadPageData();
666  }
667  return $this->mTouched;
668  }
669 
674  public function getLinksTimestamp() {
675  if ( !$this->mDataLoaded ) {
676  $this->loadPageData();
677  }
678  return $this->mLinksUpdated;
679  }
680 
685  public function getLatest() {
686  if ( !$this->mDataLoaded ) {
687  $this->loadPageData();
688  }
689  return (int)$this->mLatest;
690  }
691 
698  public function getOldestRevision() {
699  $rev = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
700  return $rev ? new Revision( $rev ) : null;
701  }
702 
707  protected function loadLastEdit() {
708  if ( $this->mLastRevision !== null ) {
709  return; // already loaded
710  }
711 
712  $latest = $this->getLatest();
713  if ( !$latest ) {
714  return; // page doesn't exist or is missing page_latest info
715  }
716 
717  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
718  // T39225: if session S1 loads the page row FOR UPDATE, the result always
719  // includes the latest changes committed. This is true even within REPEATABLE-READ
720  // transactions, where S1 normally only sees changes committed before the first S1
721  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
722  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
723  // happened after the first S1 SELECT.
724  // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
725  $revision = $this->getRevisionStore()
726  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LOCKING );
727  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
728  // Bug T93976: if page_latest was loaded from the master, fetch the
729  // revision from there as well, as it may not exist yet on a replica DB.
730  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
731  $revision = $this->getRevisionStore()
732  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LATEST );
733  } else {
734  $revision = $this->getRevisionStore()->getKnownCurrentRevision( $this->getTitle(), $latest );
735  }
736 
737  if ( $revision ) { // sanity
738  $this->setLastEdit( $revision );
739  }
740  }
741 
746  private function setLastEdit( RevisionRecord $revRecord ) {
747  // TODO mLastRevision should be replaced with RevisionRecord
748  $this->mLastRevision = new Revision( $revRecord );
749 
750  $this->mTimestamp = $revRecord->getTimestamp();
751  }
752 
757  public function getRevision() {
758  $this->loadLastEdit();
759  if ( $this->mLastRevision ) {
760  return $this->mLastRevision;
761  }
762  return null;
763  }
764 
769  public function getRevisionRecord() {
770  $this->loadLastEdit();
771  if ( $this->mLastRevision ) {
772  return $this->mLastRevision->getRevisionRecord();
773  }
774  return null;
775  }
776 
790  public function getContent( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
791  $this->loadLastEdit();
792  if ( $this->mLastRevision ) {
793  return $this->mLastRevision->getContent( $audience, $user );
794  }
795  return null;
796  }
797 
801  public function getTimestamp() {
802  // Check if the field has been filled by WikiPage::setTimestamp()
803  if ( !$this->mTimestamp ) {
804  $this->loadLastEdit();
805  }
806 
807  return MWTimestamp::convert( TS_MW, $this->mTimestamp );
808  }
809 
815  public function setTimestamp( $ts ) {
816  $this->mTimestamp = MWTimestamp::convert( TS_MW, $ts );
817  }
818 
828  public function getUser( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
829  $this->loadLastEdit();
830  if ( $this->mLastRevision ) {
831  if ( $audience === RevisionRecord::FOR_THIS_USER && $user === null ) {
832  wfDeprecated(
833  __METHOD__ . ' using FOR_THIS_USER without a user',
834  '1.35'
835  );
836  global $wgUser;
837  $user = $wgUser;
838  }
839  return $this->mLastRevision->getUser( $audience, $user );
840  } else {
841  return -1;
842  }
843  }
844 
855  public function getCreator( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
856  $revision = $this->getOldestRevision();
857  if ( $revision ) {
858  if ( $audience === RevisionRecord::FOR_THIS_USER && $user === null ) {
859  wfDeprecated(
860  __METHOD__ . ' using FOR_THIS_USER without a user',
861  '1.35'
862  );
863  global $wgUser;
864  $user = $wgUser;
865  }
866  $userName = $revision->getUserText( $audience, $user );
867  return User::newFromName( $userName, false );
868  } else {
869  return null;
870  }
871  }
872 
882  public function getUserText( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
883  $this->loadLastEdit();
884  if ( $this->mLastRevision ) {
885  if ( $audience === RevisionRecord::FOR_THIS_USER && $user === null ) {
886  wfDeprecated(
887  __METHOD__ . ' using FOR_THIS_USER without a user',
888  '1.35'
889  );
890  global $wgUser;
891  $user = $wgUser;
892  }
893  return $this->mLastRevision->getUserText( $audience, $user );
894  } else {
895  return '';
896  }
897  }
898 
909  public function getComment( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
910  $this->loadLastEdit();
911  if ( $this->mLastRevision ) {
912  if ( $audience === RevisionRecord::FOR_THIS_USER && $user === null ) {
913  wfDeprecated(
914  __METHOD__ . ' using FOR_THIS_USER without a user',
915  '1.35'
916  );
917  global $wgUser;
918  $user = $wgUser;
919  }
920  return $this->mLastRevision->getComment( $audience, $user );
921  } else {
922  return '';
923  }
924  }
925 
931  public function getMinorEdit() {
932  $this->loadLastEdit();
933  if ( $this->mLastRevision ) {
934  return $this->mLastRevision->isMinor();
935  } else {
936  return false;
937  }
938  }
939 
948  public function isCountable( $editInfo = false ) {
949  global $wgArticleCountMethod;
950 
951  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
952 
953  if ( !$this->mTitle->isContentPage() ) {
954  return false;
955  }
956 
957  if ( $editInfo ) {
958  // NOTE: only the main slot can make a page a redirect
959  $content = $editInfo->pstContent;
960  } else {
961  $content = $this->getContent();
962  }
963 
964  if ( !$content || $content->isRedirect() ) {
965  return false;
966  }
967 
968  $hasLinks = null;
969 
970  if ( $wgArticleCountMethod === 'link' ) {
971  // nasty special case to avoid re-parsing to detect links
972 
973  if ( $editInfo ) {
974  // ParserOutput::getLinks() is a 2D array of page links, so
975  // to be really correct we would need to recurse in the array
976  // but the main array should only have items in it if there are
977  // links.
978  $hasLinks = (bool)count( $editInfo->output->getLinks() );
979  } else {
980  // NOTE: keep in sync with RevisionRenderer::getLinkCount
981  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
982  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', '1',
983  [ 'pl_from' => $this->getId() ], __METHOD__ );
984  }
985  }
986 
987  // TODO: MCR: determine $hasLinks for each slot, and use that info
988  // with that slot's Content's isCountable method. That requires per-
989  // slot ParserOutput in the ParserCache, or per-slot info in the
990  // pagelinks table.
991  return $content->isCountable( $hasLinks );
992  }
993 
1001  public function getRedirectTarget() {
1002  if ( !$this->mTitle->isRedirect() ) {
1003  return null;
1004  }
1005 
1006  if ( $this->mRedirectTarget !== null ) {
1007  return $this->mRedirectTarget;
1008  }
1009 
1010  // Query the redirect table
1011  $dbr = wfGetDB( DB_REPLICA );
1012  $row = $dbr->selectRow( 'redirect',
1013  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1014  [ 'rd_from' => $this->getId() ],
1015  __METHOD__
1016  );
1017 
1018  // rd_fragment and rd_interwiki were added later, populate them if empty
1019  if ( $row && $row->rd_fragment !== null && $row->rd_interwiki !== null ) {
1020  // (T203942) We can't redirect to Media namespace because it's virtual.
1021  // We don't want to modify Title objects farther down the
1022  // line. So, let's fix this here by changing to File namespace.
1023  if ( $row->rd_namespace == NS_MEDIA ) {
1024  $namespace = NS_FILE;
1025  } else {
1026  $namespace = $row->rd_namespace;
1027  }
1028  $this->mRedirectTarget = Title::makeTitle(
1029  $namespace, $row->rd_title,
1030  $row->rd_fragment, $row->rd_interwiki
1031  );
1032  return $this->mRedirectTarget;
1033  }
1034 
1035  // This page doesn't have an entry in the redirect table
1036  $this->mRedirectTarget = $this->insertRedirect();
1037  return $this->mRedirectTarget;
1038  }
1039 
1048  public function insertRedirect() {
1049  $content = $this->getContent();
1050  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1051  if ( !$retval ) {
1052  return null;
1053  }
1054 
1055  // Update the DB post-send if the page has not cached since now
1056  $latest = $this->getLatest();
1058  function () use ( $retval, $latest ) {
1059  $this->insertRedirectEntry( $retval, $latest );
1060  },
1062  wfGetDB( DB_MASTER )
1063  );
1064 
1065  return $retval;
1066  }
1067 
1074  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1075  $dbw = wfGetDB( DB_MASTER );
1076  $dbw->startAtomic( __METHOD__ );
1077 
1078  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1079  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1080  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1081  $dbw->upsert(
1082  'redirect',
1083  [
1084  'rd_from' => $this->getId(),
1085  'rd_namespace' => $rt->getNamespace(),
1086  'rd_title' => $rt->getDBkey(),
1087  'rd_fragment' => $truncatedFragment,
1088  'rd_interwiki' => $rt->getInterwiki(),
1089  ],
1090  'rd_from',
1091  [
1092  'rd_namespace' => $rt->getNamespace(),
1093  'rd_title' => $rt->getDBkey(),
1094  'rd_fragment' => $truncatedFragment,
1095  'rd_interwiki' => $rt->getInterwiki(),
1096  ],
1097  __METHOD__
1098  );
1099  $success = true;
1100  } else {
1101  $success = false;
1102  }
1103 
1104  $dbw->endAtomic( __METHOD__ );
1105 
1106  return $success;
1107  }
1108 
1114  public function followRedirect() {
1115  return $this->getRedirectURL( $this->getRedirectTarget() );
1116  }
1117 
1125  public function getRedirectURL( $rt ) {
1126  if ( !$rt ) {
1127  return false;
1128  }
1129 
1130  if ( $rt->isExternal() ) {
1131  if ( $rt->isLocal() ) {
1132  // Offsite wikis need an HTTP redirect.
1133  // This can be hard to reverse and may produce loops,
1134  // so they may be disabled in the site configuration.
1135  $source = $this->mTitle->getFullURL( 'redirect=no' );
1136  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1137  } else {
1138  // External pages without "local" bit set are not valid
1139  // redirect targets
1140  return false;
1141  }
1142  }
1143 
1144  if ( $rt->isSpecialPage() ) {
1145  // Gotta handle redirects to special pages differently:
1146  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1147  // Some pages are not valid targets.
1148  if ( $rt->isValidRedirectTarget() ) {
1149  return $rt->getFullURL();
1150  } else {
1151  return false;
1152  }
1153  }
1154 
1155  return $rt;
1156  }
1157 
1163  public function getContributors() {
1164  // @todo: This is expensive; cache this info somewhere.
1165 
1166  $dbr = wfGetDB( DB_REPLICA );
1167 
1168  $actorMigration = ActorMigration::newMigration();
1169  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1170 
1171  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1172 
1173  $fields = [
1174  'user_id' => $actorQuery['fields']['rev_user'],
1175  'user_name' => $actorQuery['fields']['rev_user_text'],
1176  'actor_id' => $actorQuery['fields']['rev_actor'],
1177  'user_real_name' => 'MIN(user_real_name)',
1178  'timestamp' => 'MAX(rev_timestamp)',
1179  ];
1180 
1181  $conds = [ 'rev_page' => $this->getId() ];
1182 
1183  // The user who made the top revision gets credited as "this page was last edited by
1184  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1185  $user = $this->getUser()
1186  ? User::newFromId( $this->getUser() )
1187  : User::newFromName( $this->getUserText(), false );
1188  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1189 
1190  // Username hidden?
1191  $conds[] = "{$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER )} = 0";
1192 
1193  $jconds = [
1194  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1195  ] + $actorQuery['joins'];
1196 
1197  $options = [
1198  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1199  'ORDER BY' => 'timestamp DESC',
1200  ];
1201 
1202  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1203  return new UserArrayFromResult( $res );
1204  }
1205 
1213  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1214  return $parserOptions->getStubThreshold() == 0
1215  && $this->exists()
1216  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1217  && $this->getContentHandler()->isParserCacheSupported();
1218  }
1219 
1235  public function getParserOutput(
1236  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1237  ) {
1238  $useParserCache =
1239  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1240 
1241  if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1242  throw new InvalidArgumentException(
1243  'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1244  );
1245  }
1246 
1247  wfDebug( __METHOD__ .
1248  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1249  if ( $parserOptions->getStubThreshold() ) {
1250  wfIncrStats( 'pcache.miss.stub' );
1251  }
1252 
1253  if ( $useParserCache ) {
1254  $parserOutput = $this->getParserCache()
1255  ->get( $this, $parserOptions );
1256  if ( $parserOutput !== false ) {
1257  return $parserOutput;
1258  }
1259  }
1260 
1261  if ( $oldid === null || $oldid === 0 ) {
1262  $oldid = $this->getLatest();
1263  }
1264 
1265  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1266  $pool->execute();
1267 
1268  return $pool->getParserOutput();
1269  }
1270 
1276  public function doViewUpdates( User $user, $oldid = 0 ) {
1277  if ( wfReadOnly() ) {
1278  return;
1279  }
1280 
1281  // Update newtalk / watchlist notification status;
1282  // Avoid outage if the master is not reachable by using a deferred updated
1284  function () use ( $user, $oldid ) {
1285  Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1286 
1287  $user->clearNotification( $this->mTitle, $oldid );
1288  },
1290  );
1291  }
1292 
1299  public function doPurge() {
1300  // Avoid PHP 7.1 warning of passing $this by reference
1301  $wikiPage = $this;
1302 
1303  if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1304  return false;
1305  }
1306 
1307  $this->mTitle->invalidateCache();
1308 
1309  // Clear file cache
1311  // Send purge after above page_touched update was committed
1313  new CdnCacheUpdate( [ $this->mTitle ] ),
1315  );
1316 
1317  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1318  MediaWikiServices::getInstance()->getMessageCache()
1319  ->updateMessageOverride( $this->mTitle, $this->getContent() );
1320  }
1321 
1322  return true;
1323  }
1324 
1341  public function insertOn( $dbw, $pageId = null ) {
1342  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1343  $dbw->insert(
1344  'page',
1345  [
1346  'page_namespace' => $this->mTitle->getNamespace(),
1347  'page_title' => $this->mTitle->getDBkey(),
1348  'page_restrictions' => '',
1349  'page_is_redirect' => 0, // Will set this shortly...
1350  'page_is_new' => 1,
1351  'page_random' => wfRandom(),
1352  'page_touched' => $dbw->timestamp(),
1353  'page_latest' => 0, // Fill this in shortly...
1354  'page_len' => 0, // Fill this in shortly...
1355  ] + $pageIdForInsert,
1356  __METHOD__,
1357  [ 'IGNORE' ]
1358  );
1359 
1360  if ( $dbw->affectedRows() > 0 ) {
1361  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1362  $this->mId = $newid;
1363  $this->mTitle->resetArticleID( $newid );
1364 
1365  return $newid;
1366  } else {
1367  return false; // nothing changed
1368  }
1369  }
1370 
1386  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1387  $lastRevIsRedirect = null
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 ( $lastRevision !== null ) {
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  'page_content_model' => $revision->getContentModel(),
1421  ];
1422 
1423  $dbw->update( 'page',
1424  $row,
1425  $conditions,
1426  __METHOD__ );
1427 
1428  $result = $dbw->affectedRows() > 0;
1429  if ( $result ) {
1430  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1431  $this->setLastEdit( $revision->getRevisionRecord() );
1432  $this->mLatest = $revision->getId();
1433  $this->mIsRedirect = (bool)$rt;
1434  // Update the LinkCache.
1435  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1436  $linkCache->addGoodLinkObj(
1437  $this->getId(),
1438  $this->mTitle,
1439  $len,
1440  $this->mIsRedirect,
1441  $this->mLatest,
1442  $revision->getContentModel()
1443  );
1444  }
1445 
1446  return $result;
1447  }
1448 
1460  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1461  // Always update redirects (target link might have changed)
1462  // Update/Insert if we don't know if the last revision was a redirect or not
1463  // Delete if changing from redirect to non-redirect
1464  $isRedirect = $redirectTitle !== null;
1465 
1466  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1467  return true;
1468  }
1469 
1470  if ( $isRedirect ) {
1471  $success = $this->insertRedirectEntry( $redirectTitle );
1472  } else {
1473  // This is not a redirect, remove row from redirect table
1474  $where = [ 'rd_from' => $this->getId() ];
1475  $dbw->delete( 'redirect', $where, __METHOD__ );
1476  $success = true;
1477  }
1478 
1479  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1480  MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
1481  ->invalidateImageRedirect( $this->getTitle() );
1482  }
1483 
1484  return $success;
1485  }
1486 
1497  public function updateIfNewerOn( $dbw, $revision ) {
1498  $row = $dbw->selectRow(
1499  [ 'revision', 'page' ],
1500  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1501  [
1502  'page_id' => $this->getId(),
1503  'page_latest=rev_id' ],
1504  __METHOD__ );
1505 
1506  if ( $row ) {
1507  if ( MWTimestamp::convert( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1508  return false;
1509  }
1510  $prev = $row->rev_id;
1511  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1512  } else {
1513  // No or missing previous revision; mark the page as new
1514  $prev = 0;
1515  $lastRevIsRedirect = null;
1516  }
1517 
1518  $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1519 
1520  return $ret;
1521  }
1522 
1535  public static function hasDifferencesOutsideMainSlot( $a, $b ) {
1536  if ( $a instanceof Revision ) {
1537  wfDeprecated( __METHOD__ . ' with Revision objects', '1.35' );
1538  $a = $a->getRevisionRecord();
1539  }
1540  if ( $b instanceof Revision ) {
1541  wfDeprecated( __METHOD__ . ' with Revision objects', '1.35' );
1542  $b = $b->getRevisionRecord();
1543  }
1544  $aSlots = $a->getSlots();
1545  $bSlots = $b->getSlots();
1546  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1547 
1548  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1549  }
1550 
1562  public function getUndoContent( Revision $undo, Revision $undoafter ) {
1563  // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1564 
1565  if ( self::hasDifferencesOutsideMainSlot(
1566  $undo->getRevisionRecord(),
1567  $undoafter->getRevisionRecord()
1568  ) ) {
1569  // Cannot yet undo edits that involve anything other the main slot.
1570  return false;
1571  }
1572 
1573  $handler = $undo->getContentHandler();
1574  return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1575  }
1576 
1587  public function supportsSections() {
1588  return $this->getContentHandler()->supportsSections();
1589  }
1590 
1605  public function replaceSectionContent(
1606  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1607  ) {
1608  $baseRevId = null;
1609  if ( $edittime && $sectionId !== 'new' ) {
1610  $lb = $this->getDBLoadBalancer();
1611  $rev = $this->getRevisionStore()->getRevisionByTimestamp( $this->mTitle, $edittime );
1612  // Try the master if this thread may have just added it.
1613  // This could be abstracted into a Revision method, but we don't want
1614  // to encourage loading of revisions by timestamp.
1615  if ( !$rev
1616  && $lb->getServerCount() > 1
1617  && $lb->hasOrMadeRecentMasterChanges()
1618  ) {
1619  $rev = $this->getRevisionStore()->getRevisionByTimestamp(
1620  $this->mTitle, $edittime, RevisionStore::READ_LATEST );
1621  }
1622  if ( $rev ) {
1623  $baseRevId = $rev->getId();
1624  }
1625  }
1626 
1627  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1628  }
1629 
1643  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1644  $sectionTitle = '', $baseRevId = null
1645  ) {
1646  if ( strval( $sectionId ) === '' ) {
1647  // Whole-page edit; let the whole text through
1648  $newContent = $sectionContent;
1649  } else {
1650  if ( !$this->supportsSections() ) {
1651  throw new MWException( "sections not supported for content model " .
1652  $this->getContentHandler()->getModelID() );
1653  }
1654 
1655  // T32711: always use current version when adding a new section
1656  if ( $baseRevId === null || $sectionId === 'new' ) {
1657  $oldContent = $this->getContent();
1658  } else {
1659  $rev = Revision::newFromId( $baseRevId );
1660  if ( !$rev ) {
1661  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1662  $this->getId() . "; section: $sectionId)\n" );
1663  return null;
1664  }
1665 
1666  $oldContent = $rev->getContent();
1667  }
1668 
1669  if ( !$oldContent ) {
1670  wfDebug( __METHOD__ . ": no page text\n" );
1671  return null;
1672  }
1673 
1674  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1675  }
1676 
1677  return $newContent;
1678  }
1679 
1689  public function checkFlags( $flags ) {
1690  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1691  if ( $this->exists() ) {
1692  $flags |= EDIT_UPDATE;
1693  } else {
1694  $flags |= EDIT_NEW;
1695  }
1696  }
1697 
1698  return $flags;
1699  }
1700 
1704  private function newDerivedDataUpdater() {
1706 
1707  $services = MediaWikiServices::getInstance();
1709  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1710  $this->getRevisionStore(),
1711  $this->getRevisionRenderer(),
1712  $this->getSlotRoleRegistry(),
1713  $this->getParserCache(),
1715  $services->getMessageCache(),
1716  $services->getContentLanguage(),
1717  $services->getDBLoadBalancerFactory(),
1718  $this->getContentHandlerFactory()
1719  );
1720 
1721  $derivedDataUpdater->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
1724 
1725  return $derivedDataUpdater;
1726  }
1727 
1755  private function getDerivedDataUpdater(
1756  User $forUser = null,
1757  RevisionRecord $forRevision = null,
1758  RevisionSlotsUpdate $forUpdate = null,
1759  $forEdit = false
1760  ) {
1761  if ( !$forRevision && !$forUpdate ) {
1762  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1763  // going to use it with.
1764  $this->derivedDataUpdater = null;
1765  }
1766 
1767  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1768  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1769  // to it did not yet initialize it, because we don't know what data it will be
1770  // initialized with.
1771  $this->derivedDataUpdater = null;
1772  }
1773 
1774  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1775  // However, there is no good way to construct a cache key. We'd need to check against all
1776  // cached instances.
1777 
1778  if ( $this->derivedDataUpdater
1779  && !$this->derivedDataUpdater->isReusableFor(
1780  $forUser,
1781  $forRevision,
1782  $forUpdate,
1783  $forEdit ? $this->getLatest() : null
1784  )
1785  ) {
1786  $this->derivedDataUpdater = null;
1787  }
1788 
1789  if ( !$this->derivedDataUpdater ) {
1790  $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1791  }
1792 
1794  }
1795 
1811  public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1813 
1814  $pageUpdater = new PageUpdater(
1815  $user,
1816  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1817  $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1818  $this->getDBLoadBalancer(),
1819  $this->getRevisionStore(),
1820  $this->getSlotRoleRegistry(),
1821  $this->getContentHandlerFactory()
1822  );
1823 
1824  $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
1825  $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
1826  $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
1827 
1828  return $pageUpdater;
1829  }
1830 
1893  public function doEditContent(
1894  Content $content, $summary, $flags = 0, $originalRevId = false,
1895  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1896  ) {
1897  global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1898 
1899  if ( !( $summary instanceof CommentStoreComment ) ) {
1900  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1901  }
1902 
1903  if ( !$user ) {
1904  $user = $wgUser;
1905  }
1906 
1907  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1908  // Checking the minoredit right should be done in the same place the 'bot' right is
1909  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1910  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1911  if ( ( $flags & EDIT_MINOR ) && !$permissionManager->userHasRight( $user, 'minoredit' ) ) {
1912  $flags = ( $flags & ~EDIT_MINOR );
1913  }
1914 
1915  $slotsUpdate = new RevisionSlotsUpdate();
1916  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1917 
1918  // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1919  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1920  // used by this PageUpdater. However, there is no guarantee for this.
1921  $updater = $this->newPageUpdater( $user, $slotsUpdate );
1922  $updater->setContent( SlotRecord::MAIN, $content );
1923  $updater->setOriginalRevisionId( $originalRevId );
1924  $updater->setUndidRevisionId( $undidRevId );
1925 
1926  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1927 
1928  // TODO: this logic should not be in the storage layer, it's here for compatibility
1929  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1930  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1931 
1932  if ( $needsPatrol && $permissionManager->userCan(
1933  'autopatrol', $user, $this->getTitle()
1934  ) ) {
1935  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1936  }
1937 
1938  $updater->addTags( $tags );
1939 
1940  $revRec = $updater->saveRevision(
1941  $summary,
1942  $flags
1943  );
1944 
1945  // $revRec will be null if the edit failed, or if no new revision was created because
1946  // the content did not change.
1947  if ( $revRec ) {
1948  // update cached fields
1949  // TODO: this is currently redundant to what is done in updateRevisionOn.
1950  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1951  $this->setLastEdit( $revRec );
1952  $this->mLatest = $revRec->getId();
1953  }
1954 
1955  return $updater->getStatus();
1956  }
1957 
1972  public function makeParserOptions( $context ) {
1973  $options = ParserOptions::newCanonical( $context );
1974 
1975  if ( $this->getTitle()->isConversionTable() ) {
1976  // @todo ConversionTable should become a separate content model, so
1977  // we don't need special cases like this one.
1978  $options->disableContentConversion();
1979  }
1980 
1981  return $options;
1982  }
1983 
2002  public function prepareContentForEdit(
2003  Content $content,
2004  $revision = null,
2005  User $user = null,
2006  $serialFormat = null,
2007  $useCache = true
2008  ) {
2009  global $wgUser;
2010 
2011  if ( !$user ) {
2012  $user = $wgUser;
2013  }
2014 
2015  if ( $revision !== null ) {
2016  if ( $revision instanceof Revision ) {
2017  $revision = $revision->getRevisionRecord();
2018  } elseif ( !( $revision instanceof RevisionRecord ) ) {
2019  throw new InvalidArgumentException(
2020  __METHOD__ . ': invalid $revision argument type ' . gettype( $revision ) );
2021  }
2022  }
2023 
2024  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2025  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2026 
2027  if ( !$updater->isUpdatePrepared() ) {
2028  $updater->prepareContent( $user, $slots, $useCache );
2029 
2030  if ( $revision ) {
2031  $updater->prepareUpdate(
2032  $revision,
2033  [
2034  'causeAction' => 'prepare-edit',
2035  'causeAgent' => $user->getName(),
2036  ]
2037  );
2038  }
2039  }
2040 
2041  return $updater->getPreparedEdit();
2042  }
2043 
2071  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2072  $options += [
2073  'causeAction' => 'edit-page',
2074  'causeAgent' => $user->getName(),
2075  ];
2076 
2077  $revision = $revision->getRevisionRecord();
2078 
2079  $updater = $this->getDerivedDataUpdater( $user, $revision );
2080 
2081  $updater->prepareUpdate( $revision, $options );
2082 
2083  $updater->doUpdates();
2084  }
2085 
2099  public function updateParserCache( array $options = [] ) {
2100  $revision = $this->getRevisionRecord();
2101  if ( !$revision || !$revision->getId() ) {
2102  LoggerFactory::getInstance( 'wikipage' )->info(
2103  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2104  );
2105  return;
2106  }
2107  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2108 
2109  $updater = $this->getDerivedDataUpdater( $user, $revision );
2110  $updater->prepareUpdate( $revision, $options );
2111  $updater->doParserCacheUpdate();
2112  }
2113 
2143  public function doSecondaryDataUpdates( array $options = [] ) {
2144  $options['recursive'] = $options['recursive'] ?? true;
2145  $revision = $this->getRevisionRecord();
2146  if ( !$revision || !$revision->getId() ) {
2147  LoggerFactory::getInstance( 'wikipage' )->info(
2148  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2149  );
2150  return;
2151  }
2152  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2153 
2154  $updater = $this->getDerivedDataUpdater( $user, $revision );
2155  $updater->prepareUpdate( $revision, $options );
2156  $updater->doSecondaryDataUpdates( $options );
2157  }
2158 
2173  public function doUpdateRestrictions( array $limit, array $expiry,
2174  &$cascade, $reason, User $user, $tags = null
2175  ) {
2177 
2178  if ( wfReadOnly() ) {
2179  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2180  }
2181 
2182  $this->loadPageData( 'fromdbmaster' );
2183  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2184  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2185  $id = $this->getId();
2186 
2187  if ( !$cascade ) {
2188  $cascade = false;
2189  }
2190 
2191  // Take this opportunity to purge out expired restrictions
2193 
2194  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2195  // we expect a single selection, but the schema allows otherwise.
2196  $isProtected = false;
2197  $protect = false;
2198  $changed = false;
2199 
2200  $dbw = wfGetDB( DB_MASTER );
2201 
2202  foreach ( $restrictionTypes as $action ) {
2203  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2204  $expiry[$action] = 'infinity';
2205  }
2206  if ( !isset( $limit[$action] ) ) {
2207  $limit[$action] = '';
2208  } elseif ( $limit[$action] != '' ) {
2209  $protect = true;
2210  }
2211 
2212  // Get current restrictions on $action
2213  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2214  if ( $current != '' ) {
2215  $isProtected = true;
2216  }
2217 
2218  if ( $limit[$action] != $current ) {
2219  $changed = true;
2220  } elseif ( $limit[$action] != '' ) {
2221  // Only check expiry change if the action is actually being
2222  // protected, since expiry does nothing on an not-protected
2223  // action.
2224  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2225  $changed = true;
2226  }
2227  }
2228  }
2229 
2230  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2231  $changed = true;
2232  }
2233 
2234  // If nothing has changed, do nothing
2235  if ( !$changed ) {
2236  return Status::newGood();
2237  }
2238 
2239  if ( !$protect ) { // No protection at all means unprotection
2240  $revCommentMsg = 'unprotectedarticle-comment';
2241  $logAction = 'unprotect';
2242  } elseif ( $isProtected ) {
2243  $revCommentMsg = 'modifiedarticleprotection-comment';
2244  $logAction = 'modify';
2245  } else {
2246  $revCommentMsg = 'protectedarticle-comment';
2247  $logAction = 'protect';
2248  }
2249 
2250  $logRelationsValues = [];
2251  $logRelationsField = null;
2252  $logParamsDetails = [];
2253 
2254  // Null revision (used for change tag insertion)
2255  $nullRevision = null;
2256 
2257  if ( $id ) { // Protection of existing page
2258  // Avoid PHP 7.1 warning of passing $this by reference
2259  $wikiPage = $this;
2260 
2261  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2262  return Status::newGood();
2263  }
2264 
2265  // Only certain restrictions can cascade...
2266  $editrestriction = isset( $limit['edit'] )
2267  ? [ $limit['edit'] ]
2268  : $this->mTitle->getRestrictions( 'edit' );
2269  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2270  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2271  }
2272  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2273  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2274  }
2275 
2276  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2277  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2278  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2279  }
2280  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2281  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2282  }
2283 
2284  // The schema allows multiple restrictions
2285  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2286  $cascade = false;
2287  }
2288 
2289  // insert null revision to identify the page protection change as edit summary
2290  $latest = $this->getLatest();
2291  $nullRevision = $this->insertProtectNullRevision(
2292  $revCommentMsg,
2293  $limit,
2294  $expiry,
2295  $cascade,
2296  $reason,
2297  $user
2298  );
2299 
2300  if ( $nullRevision === null ) {
2301  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2302  }
2303 
2304  $logRelationsField = 'pr_id';
2305 
2306  // T214035: Avoid deadlock on MySQL.
2307  // Do a DELETE by primary key (pr_id) for any existing protection rows.
2308  // On MySQL and derivatives, unconditionally deleting by page ID (pr_page) would.
2309  // place a gap lock if there are no matching rows. This can deadlock when another
2310  // thread modifies protection settings for page IDs in the same gap.
2311  $existingProtectionIds = $dbw->selectFieldValues(
2312  'page_restrictions',
2313  'pr_id',
2314  [
2315  'pr_page' => $id,
2316  'pr_type' => array_map( 'strval', array_keys( $limit ) )
2317  ],
2318  __METHOD__
2319  );
2320 
2321  if ( $existingProtectionIds ) {
2322  $dbw->delete(
2323  'page_restrictions',
2324  [ 'pr_id' => $existingProtectionIds ],
2325  __METHOD__
2326  );
2327  }
2328 
2329  // Update restrictions table
2330  foreach ( $limit as $action => $restrictions ) {
2331  if ( $restrictions != '' ) {
2332  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2333  $dbw->insert(
2334  'page_restrictions',
2335  [
2336  'pr_page' => $id,
2337  'pr_type' => $action,
2338  'pr_level' => $restrictions,
2339  'pr_cascade' => $cascadeValue,
2340  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2341  ],
2342  __METHOD__
2343  );
2344  $logRelationsValues[] = $dbw->insertId();
2345  $logParamsDetails[] = [
2346  'type' => $action,
2347  'level' => $restrictions,
2348  'expiry' => $expiry[$action],
2349  'cascade' => (bool)$cascadeValue,
2350  ];
2351  }
2352  }
2353 
2354  // Clear out legacy restriction fields
2355  $dbw->update(
2356  'page',
2357  [ 'page_restrictions' => '' ],
2358  [ 'page_id' => $id ],
2359  __METHOD__
2360  );
2361 
2362  // Avoid PHP 7.1 warning of passing $this by reference
2363  $wikiPage = $this;
2364 
2365  Hooks::run( 'NewRevisionFromEditComplete',
2366  [ $this, $nullRevision, $latest, $user, &$tags ] );
2367  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2368  } else { // Protection of non-existing page (also known as "title protection")
2369  // Cascade protection is meaningless in this case
2370  $cascade = false;
2371 
2372  if ( $limit['create'] != '' ) {
2373  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2374  $dbw->replace( 'protected_titles',
2375  [ [ 'pt_namespace', 'pt_title' ] ],
2376  [
2377  'pt_namespace' => $this->mTitle->getNamespace(),
2378  'pt_title' => $this->mTitle->getDBkey(),
2379  'pt_create_perm' => $limit['create'],
2380  'pt_timestamp' => $dbw->timestamp(),
2381  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2382  'pt_user' => $user->getId(),
2383  ] + $commentFields, __METHOD__
2384  );
2385  $logParamsDetails[] = [
2386  'type' => 'create',
2387  'level' => $limit['create'],
2388  'expiry' => $expiry['create'],
2389  ];
2390  } else {
2391  $dbw->delete( 'protected_titles',
2392  [
2393  'pt_namespace' => $this->mTitle->getNamespace(),
2394  'pt_title' => $this->mTitle->getDBkey()
2395  ], __METHOD__
2396  );
2397  }
2398  }
2399 
2400  $this->mTitle->flushRestrictions();
2401  InfoAction::invalidateCache( $this->mTitle );
2402 
2403  if ( $logAction == 'unprotect' ) {
2404  $params = [];
2405  } else {
2406  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2407  $params = [
2408  '4::description' => $protectDescriptionLog, // parameter for IRC
2409  '5:bool:cascade' => $cascade,
2410  'details' => $logParamsDetails, // parameter for localize and api
2411  ];
2412  }
2413 
2414  // Update the protection log
2415  $logEntry = new ManualLogEntry( 'protect', $logAction );
2416  $logEntry->setTarget( $this->mTitle );
2417  $logEntry->setComment( $reason );
2418  $logEntry->setPerformer( $user );
2419  $logEntry->setParameters( $params );
2420  if ( $nullRevision !== null ) {
2421  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2422  }
2423  $logEntry->addTags( $tags );
2424  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2425  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2426  }
2427  $logId = $logEntry->insert();
2428  $logEntry->publish( $logId );
2429 
2430  return Status::newGood( $logId );
2431  }
2432 
2444  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2445  array $expiry, $cascade, $reason, $user = null
2446  ) {
2447  if ( !$user ) {
2448  wfDeprecated( __METHOD__ . ' without passing a $user parameter', '1.35' );
2449  // Don't need to set to $wgUser here, handled in Revision::newNullRevision
2450  // where the user is needed
2451  }
2452 
2453  $dbw = wfGetDB( DB_MASTER );
2454 
2455  // Prepare a null revision to be added to the history
2456  $editComment = wfMessage(
2457  $revCommentMsg,
2458  $this->mTitle->getPrefixedText(),
2459  $user ? $user->getName() : ''
2460  )->inContentLanguage()->text();
2461  if ( $reason ) {
2462  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2463  }
2464  $protectDescription = $this->protectDescription( $limit, $expiry );
2465  if ( $protectDescription ) {
2466  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2467  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2468  ->inContentLanguage()->text();
2469  }
2470  if ( $cascade ) {
2471  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2472  $editComment .= wfMessage( 'brackets' )->params(
2473  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2474  )->inContentLanguage()->text();
2475  }
2476 
2477  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2478  if ( $nullRev ) {
2479  $nullRev->insertOn( $dbw );
2480 
2481  // Update page record and touch page
2482  $oldLatest = $nullRev->getParentId();
2483  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2484  }
2485 
2486  return $nullRev;
2487  }
2488 
2493  protected function formatExpiry( $expiry ) {
2494  if ( $expiry != 'infinity' ) {
2495  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2496  return wfMessage(
2497  'protect-expiring',
2498  $contLang->timeanddate( $expiry, false, false ),
2499  $contLang->date( $expiry, false, false ),
2500  $contLang->time( $expiry, false, false )
2501  )->inContentLanguage()->text();
2502  } else {
2503  return wfMessage( 'protect-expiry-indefinite' )
2504  ->inContentLanguage()->text();
2505  }
2506  }
2507 
2515  public function protectDescription( array $limit, array $expiry ) {
2516  $protectDescription = '';
2517 
2518  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2519  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2520  # All possible message keys are listed here for easier grepping:
2521  # * restriction-create
2522  # * restriction-edit
2523  # * restriction-move
2524  # * restriction-upload
2525  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2526  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2527  # with '' filtered out. All possible message keys are listed below:
2528  # * protect-level-autoconfirmed
2529  # * protect-level-sysop
2530  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2531  ->inContentLanguage()->text();
2532 
2533  $expiryText = $this->formatExpiry( $expiry[$action] );
2534 
2535  if ( $protectDescription !== '' ) {
2536  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2537  }
2538  $protectDescription .= wfMessage( 'protect-summary-desc' )
2539  ->params( $actionText, $restrictionsText, $expiryText )
2540  ->inContentLanguage()->text();
2541  }
2542 
2543  return $protectDescription;
2544  }
2545 
2557  public function protectDescriptionLog( array $limit, array $expiry ) {
2558  $protectDescriptionLog = '';
2559 
2560  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2561  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2562  $expiryText = $this->formatExpiry( $expiry[$action] );
2563  $protectDescriptionLog .=
2564  $dirMark .
2565  "[$action=$restrictions] ($expiryText)";
2566  }
2567 
2568  return trim( $protectDescriptionLog );
2569  }
2570 
2583  public function isBatchedDelete( $safetyMargin = 0 ) {
2585 
2586  $dbr = wfGetDB( DB_REPLICA );
2587  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2588  $revCount += $safetyMargin;
2589 
2590  return $revCount >= $wgDeleteRevisionsBatchSize;
2591  }
2592 
2614  public function doDeleteArticle(
2615  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2616  $immediate = false
2617  ) {
2618  wfDeprecated( __METHOD__, '1.35' );
2619  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2620  [], 'delete', $immediate );
2621 
2622  // Returns true if the page was actually deleted, or is scheduled for deletion
2623  return $status->isOK();
2624  }
2625 
2650  public function doDeleteArticleReal(
2651  $reason, $user = false, $suppress = false, $u2 = null, &$error = '', User $deleter = null,
2652  $tags = [], $logsubtype = 'delete', $immediate = false
2653  ) {
2654  wfDebug( __METHOD__ . "\n" );
2655 
2656  if ( $user instanceof User ) {
2657  $deleter = $user;
2658  } else {
2659  wfDeprecated(
2660  __METHOD__ . ' without passing a User as the second parameter',
2661  '1.35'
2662  );
2663  $suppress = $user;
2664  if ( $deleter === null ) {
2665  global $wgUser;
2666  $deleter = $wgUser;
2667  }
2668  }
2669  unset( $user );
2670 
2671  $status = Status::newGood();
2672 
2673  // Avoid PHP 7.1 warning of passing $this by reference
2674  $wikiPage = $this;
2675  if ( !Hooks::run( 'ArticleDelete',
2676  [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2677  ) ) {
2678  if ( $status->isOK() ) {
2679  // Hook aborted but didn't set a fatal status
2680  $status->fatal( 'delete-hook-aborted' );
2681  }
2682  return $status;
2683  }
2684 
2685  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2686  $logsubtype, $immediate );
2687  }
2688 
2705  public function doDeleteArticleBatched(
2706  $reason, $suppress, User $deleter, $tags,
2707  $logsubtype, $immediate = false, $webRequestId = null
2708  ) {
2709  wfDebug( __METHOD__ . "\n" );
2710 
2711  $status = Status::newGood();
2712 
2713  $dbw = wfGetDB( DB_MASTER );
2714  $dbw->startAtomic( __METHOD__ );
2715 
2716  $this->loadPageData( self::READ_LATEST );
2717  $id = $this->getId();
2718  // T98706: lock the page from various other updates but avoid using
2719  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2720  // the revisions queries (which also JOIN on user). Only lock the page
2721  // row and CAS check on page_latest to see if the trx snapshot matches.
2722  $lockedLatest = $this->lockAndGetLatest();
2723  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2724  $dbw->endAtomic( __METHOD__ );
2725  // Page not there or trx snapshot is stale
2726  $status->error( 'cannotdelete',
2727  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2728  return $status;
2729  }
2730 
2731  // At this point we are now committed to returning an OK
2732  // status unless some DB query error or other exception comes up.
2733  // This way callers don't have to call rollback() if $status is bad
2734  // unless they actually try to catch exceptions (which is rare).
2735 
2736  // we need to remember the old content so we can use it to generate all deletion updates.
2737  $revision = $this->getRevision();
2738  try {
2739  $content = $this->getContent( RevisionRecord::RAW );
2740  } catch ( Exception $ex ) {
2741  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2742  . $ex->getMessage() );
2743 
2744  $content = null;
2745  }
2746 
2747  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2748  // one batch of revisions and defer archival of any others to the job queue.
2749  $explictTrxLogged = false;
2750  while ( true ) {
2751  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2752  if ( $done || !$immediate ) {
2753  break;
2754  }
2755  $dbw->endAtomic( __METHOD__ );
2756  if ( $dbw->explicitTrxActive() ) {
2757  // Explict transactions may never happen here in practice. Log to be sure.
2758  if ( !$explictTrxLogged ) {
2759  $explictTrxLogged = true;
2760  LoggerFactory::getInstance( 'wfDebug' )->debug(
2761  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2762  'title' => $this->getTitle()->getText(),
2763  ] );
2764  }
2765  continue;
2766  }
2767  if ( $dbw->trxLevel() ) {
2768  $dbw->commit();
2769  }
2770  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2771  $lbFactory->waitForReplication();
2772  $dbw->startAtomic( __METHOD__ );
2773  }
2774 
2775  // If done archiving, also delete the article.
2776  if ( !$done ) {
2777  $dbw->endAtomic( __METHOD__ );
2778 
2779  $jobParams = [
2780  'namespace' => $this->getTitle()->getNamespace(),
2781  'title' => $this->getTitle()->getDBkey(),
2782  'wikiPageId' => $id,
2783  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2784  'reason' => $reason,
2785  'suppress' => $suppress,
2786  'userId' => $deleter->getId(),
2787  'tags' => json_encode( $tags ),
2788  'logsubtype' => $logsubtype,
2789  ];
2790 
2791  $job = new DeletePageJob( $jobParams );
2792  JobQueueGroup::singleton()->push( $job );
2793 
2794  $status->warning( 'delete-scheduled',
2795  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2796  } else {
2797  // Get archivedRevisionCount by db query, because there's no better alternative.
2798  // Jobs cannot pass a count of archived revisions to the next job, because additional
2799  // deletion operations can be started while the first is running. Jobs from each
2800  // gracefully interleave, but would not know about each other's count. Deduplication
2801  // in the job queue to avoid simultaneous deletion operations would add overhead.
2802  // Number of archived revisions cannot be known beforehand, because edits can be made
2803  // while deletion operations are being processed, changing the number of archivals.
2804  $archivedRevisionCount = (int)$dbw->selectField(
2805  'archive', 'COUNT(*)',
2806  [
2807  'ar_namespace' => $this->getTitle()->getNamespace(),
2808  'ar_title' => $this->getTitle()->getDBkey(),
2809  'ar_page_id' => $id
2810  ], __METHOD__
2811  );
2812 
2813  // Clone the title and wikiPage, so we have the information we need when
2814  // we log and run the ArticleDeleteComplete hook.
2815  $logTitle = clone $this->mTitle;
2816  $wikiPageBeforeDelete = clone $this;
2817 
2818  // Now that it's safely backed up, delete it
2819  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2820 
2821  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2822  $logtype = $suppress ? 'suppress' : 'delete';
2823 
2824  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2825  $logEntry->setPerformer( $deleter );
2826  $logEntry->setTarget( $logTitle );
2827  $logEntry->setComment( $reason );
2828  $logEntry->addTags( $tags );
2829  $logid = $logEntry->insert();
2830 
2831  $dbw->onTransactionPreCommitOrIdle(
2832  function () use ( $logEntry, $logid ) {
2833  // T58776: avoid deadlocks (especially from FileDeleteForm)
2834  $logEntry->publish( $logid );
2835  },
2836  __METHOD__
2837  );
2838 
2839  $dbw->endAtomic( __METHOD__ );
2840 
2841  $this->doDeleteUpdates(
2842  $id,
2843  $content,
2844  $revision->getRevisionRecord(),
2845  $deleter
2846  );
2847 
2848  Hooks::run( 'ArticleDeleteComplete', [
2849  &$wikiPageBeforeDelete,
2850  &$deleter,
2851  $reason,
2852  $id,
2853  $content,
2854  $logEntry,
2855  $archivedRevisionCount
2856  ] );
2857  $status->value = $logid;
2858 
2859  // Show log excerpt on 404 pages rather than just a link
2860  $dbCache = ObjectCache::getInstance( 'db-replicated' );
2861  $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2862  $dbCache->set( $key, 1, $dbCache::TTL_DAY );
2863  }
2864 
2865  return $status;
2866  }
2867 
2877  protected function archiveRevisions( $dbw, $id, $suppress ) {
2879 
2880  // Given the lock above, we can be confident in the title and page ID values
2881  $namespace = $this->getTitle()->getNamespace();
2882  $dbKey = $this->getTitle()->getDBkey();
2883 
2884  $commentStore = CommentStore::getStore();
2885  $actorMigration = ActorMigration::newMigration();
2886 
2887  $revQuery = $this->getRevisionStore()->getQueryInfo();
2888  $bitfield = false;
2889 
2890  // Bitfields to further suppress the content
2891  if ( $suppress ) {
2892  $bitfield = RevisionRecord::SUPPRESSED_ALL;
2893  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2894  }
2895 
2896  // For now, shunt the revision data into the archive table.
2897  // Text is *not* removed from the text table; bulk storage
2898  // is left intact to avoid breaking block-compression or
2899  // immutable storage schemes.
2900  // In the future, we may keep revisions and mark them with
2901  // the rev_deleted field, which is reserved for this purpose.
2902 
2903  // Lock rows in `revision` and its temp tables, but not any others.
2904  // Note array_intersect() preserves keys from the first arg, and we're
2905  // assuming $revQuery has `revision` primary and isn't using subtables
2906  // for anything we care about.
2907  $dbw->lockForUpdate(
2908  array_intersect(
2909  $revQuery['tables'],
2910  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2911  ),
2912  [ 'rev_page' => $id ],
2913  __METHOD__,
2914  [],
2915  $revQuery['joins']
2916  );
2917 
2918  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2919  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2920  $res = $dbw->select(
2921  $revQuery['tables'],
2922  $revQuery['fields'],
2923  [ 'rev_page' => $id ],
2924  __METHOD__,
2925  [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2926  $revQuery['joins']
2927  );
2928 
2929  // Build their equivalent archive rows
2930  $rowsInsert = [];
2931  $revids = [];
2932 
2934  $ipRevIds = [];
2935 
2936  $done = true;
2937  foreach ( $res as $row ) {
2938  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2939  $done = false;
2940  break;
2941  }
2942 
2943  $comment = $commentStore->getComment( 'rev_comment', $row );
2944  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2945  $rowInsert = [
2946  'ar_namespace' => $namespace,
2947  'ar_title' => $dbKey,
2948  'ar_timestamp' => $row->rev_timestamp,
2949  'ar_minor_edit' => $row->rev_minor_edit,
2950  'ar_rev_id' => $row->rev_id,
2951  'ar_parent_id' => $row->rev_parent_id,
2952  'ar_len' => $row->rev_len,
2953  'ar_page_id' => $id,
2954  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2955  'ar_sha1' => $row->rev_sha1,
2956  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2957  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2958 
2959  $rowsInsert[] = $rowInsert;
2960  $revids[] = $row->rev_id;
2961 
2962  // Keep track of IP edits, so that the corresponding rows can
2963  // be deleted in the ip_changes table.
2964  if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
2965  $ipRevIds[] = $row->rev_id;
2966  }
2967  }
2968 
2969  // This conditional is just a sanity check
2970  if ( count( $revids ) > 0 ) {
2971  // Copy them into the archive table
2972  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2973 
2974  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2975  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2976  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2977 
2978  // Also delete records from ip_changes as applicable.
2979  if ( count( $ipRevIds ) > 0 ) {
2980  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2981  }
2982  }
2983 
2984  return $done;
2985  }
2986 
2993  public function lockAndGetLatest() {
2994  return (int)wfGetDB( DB_MASTER )->selectField(
2995  'page',
2996  'page_latest',
2997  [
2998  'page_id' => $this->getId(),
2999  // Typically page_id is enough, but some code might try to do
3000  // updates assuming the title is the same, so verify that
3001  'page_namespace' => $this->getTitle()->getNamespace(),
3002  'page_title' => $this->getTitle()->getDBkey()
3003  ],
3004  __METHOD__,
3005  [ 'FOR UPDATE' ]
3006  );
3007  }
3008 
3022  public function doDeleteUpdates(
3023  $id, Content $content = null, $revRecord = null, User $user = null
3024  ) {
3025  if ( $revRecord && $revRecord instanceof Revision ) {
3026  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
3027  $revRecord = $revRecord->getRevisionRecord();
3028  }
3029 
3030  if ( $id !== $this->getId() ) {
3031  throw new InvalidArgumentException( 'Mismatching page ID' );
3032  }
3033 
3034  try {
3035  $countable = $this->isCountable();
3036  } catch ( Exception $ex ) {
3037  // fallback for deleting broken pages for which we cannot load the content for
3038  // some reason. Note that doDeleteArticleReal() already logged this problem.
3039  $countable = false;
3040  }
3041 
3042  // Update site status
3044  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3045  ) );
3046 
3047  // Delete pagelinks, update secondary indexes, etc
3048  $updates = $this->getDeletionUpdates( $revRecord ?: $content );
3049  foreach ( $updates as $update ) {
3050  DeferredUpdates::addUpdate( $update );
3051  }
3052 
3053  $causeAgent = $user ? $user->getName() : 'unknown';
3054  // Reparse any pages transcluding this page
3056  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3057  // Reparse any pages including this image
3058  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3060  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3061  }
3062 
3063  // Clear caches
3064  self::onArticleDelete( $this->mTitle );
3065 
3066  // TODO use RevisionRecord here
3068  $this->mTitle,
3069  new Revision( $revRecord ),
3070  null,
3072  );
3073 
3074  // Reset this object and the Title object
3075  $this->loadFromRow( false, self::READ_LATEST );
3076 
3077  // Search engine
3078  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3079  }
3080 
3110  public function doRollback(
3111  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3112  ) {
3113  $resultDetails = null;
3114 
3115  // Check permissions
3116  $permManager = MediaWikiServices::getInstance()->getPermissionManager();
3117  $editErrors = $permManager->getPermissionErrors( 'edit', $user, $this->mTitle );
3118  $rollbackErrors = $permManager->getPermissionErrors( 'rollback', $user, $this->mTitle );
3119  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3120 
3121  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3122  $errors[] = [ 'sessionfailure' ];
3123  }
3124 
3125  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3126  $errors[] = [ 'actionthrottledtext' ];
3127  }
3128 
3129  // If there were errors, bail out now
3130  if ( !empty( $errors ) ) {
3131  return $errors;
3132  }
3133 
3134  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3135  }
3136 
3157  public function commitRollback( $fromP, $summary, $bot,
3158  &$resultDetails, User $guser, $tags = null
3159  ) {
3161 
3162  $dbw = wfGetDB( DB_MASTER );
3163 
3164  if ( wfReadOnly() ) {
3165  return [ [ 'readonlytext' ] ];
3166  }
3167 
3168  // Begin revision creation cycle by creating a PageUpdater.
3169  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3170  $updater = $this->newPageUpdater( $guser );
3171  $current = $updater->grabParentRevision();
3172 
3173  if ( $current === null ) {
3174  // Something wrong... no page?
3175  return [ [ 'notanarticle' ] ];
3176  }
3177 
3178  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3179  $legacyCurrent = new Revision( $current );
3180  $from = str_replace( '_', ' ', $fromP );
3181 
3182  // User name given should match up with the top revision.
3183  // If the revision's user is not visible, then $from should be empty.
3184  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3185  $resultDetails = [ 'current' => $legacyCurrent ];
3186  return [ [ 'alreadyrolled',
3187  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3188  htmlspecialchars( $fromP ),
3189  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3190  ] ];
3191  }
3192 
3193  // Get the last edit not by this person...
3194  // Note: these may not be public values
3195  $actorWhere = ActorMigration::newMigration()->getWhere(
3196  $dbw,
3197  'rev_user',
3198  $current->getUser( RevisionRecord::RAW )
3199  );
3200 
3201  $s = $dbw->selectRow(
3202  [ 'revision' ] + $actorWhere['tables'],
3203  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3204  [
3205  'rev_page' => $current->getPageId(),
3206  'NOT(' . $actorWhere['conds'] . ')',
3207  ],
3208  __METHOD__,
3209  [
3210  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3211  'ORDER BY' => [ 'rev_timestamp DESC', 'rev_id DESC' ]
3212  ],
3213  $actorWhere['joins']
3214  );
3215  if ( $s === false ) {
3216  // No one else ever edited this page
3217  return [ [ 'cantrollback' ] ];
3218  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3219  || $s->rev_deleted & RevisionRecord::DELETED_USER
3220  ) {
3221  // Only admins can see this text
3222  return [ [ 'notvisiblerev' ] ];
3223  }
3224 
3225  // Generate the edit summary if necessary
3226  $target = $this->getRevisionStore()->getRevisionById(
3227  $s->rev_id,
3228  RevisionStore::READ_LATEST
3229  );
3230  if ( empty( $summary ) ) {
3231  if ( !$currentEditorForPublic ) { // no public user name
3232  $summary = wfMessage( 'revertpage-nouser' );
3233  } elseif ( $wgDisableAnonTalk && $current->getUser() === 0 ) {
3234  $summary = wfMessage( 'revertpage-anon' );
3235  } else {
3236  $summary = wfMessage( 'revertpage' );
3237  }
3238  }
3239  $legacyTarget = new Revision( $target );
3240  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3241 
3242  // Allow the custom summary to use the same args as the default message
3243  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3244  $args = [
3245  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3246  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3247  $s->rev_id,
3248  $contLang->timeanddate( MWTimestamp::convert( TS_MW, $s->rev_timestamp ) ),
3249  $current->getId(),
3250  $contLang->timeanddate( $current->getTimestamp() )
3251  ];
3252  if ( $summary instanceof Message ) {
3253  $summary = $summary->params( $args )->inContentLanguage()->text();
3254  } else {
3255  $summary = wfMsgReplaceArgs( $summary, $args );
3256  }
3257 
3258  // Trim spaces on user supplied text
3259  $summary = trim( $summary );
3260 
3261  // Save
3262  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3263 
3264  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
3265  if ( $permissionManager->userHasRight( $guser, 'minoredit' ) ) {
3266  $flags |= EDIT_MINOR;
3267  }
3268 
3269  if ( $bot && ( $permissionManager->userHasAnyRight( $guser, 'markbotedits', 'bot' ) ) ) {
3270  $flags |= EDIT_FORCE_BOT;
3271  }
3272 
3273  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3274  $currentContent = $current->getContent( SlotRecord::MAIN );
3275  $targetContent = $target->getContent( SlotRecord::MAIN );
3276  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3277 
3278  if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3279  $tags[] = 'mw-rollback';
3280  }
3281 
3282  // Build rollback revision:
3283  // Restore old content
3284  // TODO: MCR: test this once we can store multiple slots
3285  foreach ( $target->getSlots()->getSlots() as $slot ) {
3286  $updater->inheritSlot( $slot );
3287  }
3288 
3289  // Remove extra slots
3290  // TODO: MCR: test this once we can store multiple slots
3291  foreach ( $current->getSlotRoles() as $role ) {
3292  if ( !$target->hasSlot( $role ) ) {
3293  $updater->removeSlot( $role );
3294  }
3295  }
3296 
3297  $updater->setOriginalRevisionId( $target->getId() );
3298  // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3299  $updater->addTags( $tags );
3300 
3301  // TODO: this logic should not be in the storage layer, it's here for compatibility
3302  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3303  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3304 
3305  if ( $wgUseRCPatrol && $permissionManager->userCan(
3306  'autopatrol', $guser, $this->getTitle()
3307  ) ) {
3308  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3309  }
3310 
3311  // Actually store the rollback
3312  $rev = $updater->saveRevision(
3314  $flags
3315  );
3316 
3317  // Set patrolling and bot flag on the edits, which gets rollbacked.
3318  // This is done even on edit failure to have patrolling in that case (T64157).
3319  $set = [];
3320  if ( $bot && $permissionManager->userHasRight( $guser, 'markbotedits' ) ) {
3321  // Mark all reverted edits as bot
3322  $set['rc_bot'] = 1;
3323  }
3324 
3325  if ( $wgUseRCPatrol ) {
3326  // Mark all reverted edits as patrolled
3327  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3328  }
3329 
3330  if ( count( $set ) ) {
3331  $actorWhere = ActorMigration::newMigration()->getWhere(
3332  $dbw,
3333  'rc_user',
3334  $current->getUser( RevisionRecord::RAW ),
3335  false
3336  );
3337  $dbw->update( 'recentchanges', $set,
3338  [ /* WHERE */
3339  'rc_cur_id' => $current->getPageId(),
3340  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3341  $actorWhere['conds'], // No tables/joins are needed for rc_user
3342  ],
3343  __METHOD__
3344  );
3345  }
3346 
3347  if ( !$updater->wasSuccessful() ) {
3348  return $updater->getStatus()->getErrorsArray();
3349  }
3350 
3351  // Report if the edit was not created because it did not change the content.
3352  if ( $updater->isUnchanged() ) {
3353  $resultDetails = [ 'current' => $legacyCurrent ];
3354  return [ [ 'alreadyrolled',
3355  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3356  htmlspecialchars( $fromP ),
3357  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3358  ] ];
3359  }
3360 
3361  if ( $changingContentModel ) {
3362  // If the content model changed during the rollback,
3363  // make sure it gets logged to Special:Log/contentmodel
3364  $log = new ManualLogEntry( 'contentmodel', 'change' );
3365  $log->setPerformer( $guser );
3366  $log->setTarget( $this->mTitle );
3367  $log->setComment( $summary );
3368  $log->setParameters( [
3369  '4::oldmodel' => $currentContent->getModel(),
3370  '5::newmodel' => $targetContent->getModel(),
3371  ] );
3372 
3373  $logId = $log->insert( $dbw );
3374  $log->publish( $logId );
3375  }
3376 
3377  $revId = $rev->getId();
3378 
3379  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3380 
3381  $resultDetails = [
3382  'summary' => $summary,
3383  'current' => $legacyCurrent,
3384  'target' => $legacyTarget,
3385  'newid' => $revId,
3386  'tags' => $tags
3387  ];
3388 
3389  // TODO: make this return a Status object and wrap $resultDetails in that.
3390  return [];
3391  }
3392 
3404  public static function onArticleCreate( Title $title ) {
3405  // TODO: move this into a PageEventEmitter service
3406 
3407  // Update existence markers on article/talk tabs...
3408  $other = $title->getOtherPage();
3409 
3410  $other->purgeSquid();
3411 
3412  $title->touchLinks();
3413  $title->purgeSquid();
3414  $title->deleteTitleProtection();
3415 
3416  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3417 
3418  // Invalidate caches of articles which include this page
3420  $title,
3421  'templatelinks',
3422  [ 'causeAction' => 'page-create' ]
3423  );
3424  JobQueueGroup::singleton()->lazyPush( $job );
3425 
3426  if ( $title->getNamespace() == NS_CATEGORY ) {
3427  // Load the Category object, which will schedule a job to create
3428  // the category table row if necessary. Checking a replica DB is ok
3429  // here, in the worst case it'll run an unnecessary recount job on
3430  // a category that probably doesn't have many members.
3431  Category::newFromTitle( $title )->getID();
3432  }
3433  }
3434 
3440  public static function onArticleDelete( Title $title ) {
3441  // TODO: move this into a PageEventEmitter service
3442 
3443  // Update existence markers on article/talk tabs...
3444  // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3445  BacklinkCache::get( $title )->clear();
3446  $other = $title->getOtherPage();
3447 
3448  $other->purgeSquid();
3449 
3450  $title->touchLinks();
3451  $title->purgeSquid();
3452 
3453  $services = MediaWikiServices::getInstance();
3454  $services->getLinkCache()->invalidateTitle( $title );
3455 
3456  // File cache
3459 
3460  // Messages
3461  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3462  $services->getMessageCache()->updateMessageOverride( $title, null );
3463  }
3464 
3465  // Images
3466  if ( $title->getNamespace() == NS_FILE ) {
3468  $title,
3469  'imagelinks',
3470  [ 'causeAction' => 'page-delete' ]
3471  );
3472  JobQueueGroup::singleton()->lazyPush( $job );
3473  }
3474 
3475  // User talk pages
3476  if ( $title->getNamespace() == NS_USER_TALK ) {
3477  $user = User::newFromName( $title->getText(), false );
3478  if ( $user ) {
3479  $user->setNewtalk( false );
3480  }
3481  }
3482 
3483  // Image redirects
3484  $services->getRepoGroup()->getLocalRepo()->invalidateImageRedirect( $title );
3485 
3486  // Purge cross-wiki cache entities referencing this page
3488  }
3489 
3498  public static function onArticleEdit(
3499  Title $title,
3500  Revision $revision = null,
3501  $slotsChanged = null
3502  ) {
3503  // TODO: move this into a PageEventEmitter service
3504  $jobs = [];
3505  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3506  // Invalidate caches of articles which include this page.
3507  // Only for the main slot, because only the main slot is transcluded.
3508  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3510  $title,
3511  'templatelinks',
3512  [ 'causeAction' => 'page-edit' ]
3513  );
3514  }
3515  // Invalidate the caches of all pages which redirect here
3517  $title,
3518  'redirect',
3519  [ 'causeAction' => 'page-edit' ]
3520  );
3521  JobQueueGroup::singleton()->lazyPush( $jobs );
3522 
3523  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3524 
3525  // Purge CDN for this page only
3526  $title->purgeSquid();
3527  // Clear file cache for this page only
3529 
3530  // Purge ?action=info cache
3531  $revid = $revision ? $revision->getId() : null;
3532  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3534  } );
3535 
3536  // Purge cross-wiki cache entities referencing this page
3538  }
3539 
3547  private static function purgeInterwikiCheckKey( Title $title ) {
3549 
3550  if ( !$wgEnableScaryTranscluding ) {
3551  return; // @todo: perhaps this wiki is only used as a *source* for content?
3552  }
3553 
3554  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3555  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3556  $cache->resetCheckKey(
3557  // Do not include the namespace since there can be multiple aliases to it
3558  // due to different namespace text definitions on different wikis. This only
3559  // means that some cache invalidations happen that are not strictly needed.
3560  $cache->makeGlobalKey(
3561  'interwiki-page',
3563  $title->getDBkey()
3564  )
3565  );
3566  } );
3567  }
3568 
3575  public function getCategories() {
3576  $id = $this->getId();
3577  if ( $id == 0 ) {
3578  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3579  }
3580 
3581  $dbr = wfGetDB( DB_REPLICA );
3582  $res = $dbr->select( 'categorylinks',
3583  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3584  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3585  // as not being aliases, and NS_CATEGORY is numeric
3586  [ 'cl_from' => $id ],
3587  __METHOD__ );
3588 
3589  return TitleArray::newFromResult( $res );
3590  }
3591 
3598  public function getHiddenCategories() {
3599  $result = [];
3600  $id = $this->getId();
3601 
3602  if ( $id == 0 ) {
3603  return [];
3604  }
3605 
3606  $dbr = wfGetDB( DB_REPLICA );
3607  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3608  [ 'cl_to' ],
3609  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3610  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3611  __METHOD__ );
3612 
3613  if ( $res !== false ) {
3614  foreach ( $res as $row ) {
3615  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3616  }
3617  }
3618 
3619  return $result;
3620  }
3621 
3629  public function getAutoDeleteReason( &$hasHistory ) {
3630  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3631  }
3632 
3643  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3644  $id = $id ?: $this->getId();
3645  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3646  getCategoryLinkType( $this->getTitle()->getNamespace() );
3647 
3648  $addFields = [ 'cat_pages = cat_pages + 1' ];
3649  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3650  if ( $type !== 'page' ) {
3651  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3652  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3653  }
3654 
3655  $dbw = wfGetDB( DB_MASTER );
3656 
3657  if ( count( $added ) ) {
3658  $existingAdded = $dbw->selectFieldValues(
3659  'category',
3660  'cat_title',
3661  [ 'cat_title' => $added ],
3662  __METHOD__
3663  );
3664 
3665  // For category rows that already exist, do a plain
3666  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3667  // to avoid creating gaps in the cat_id sequence.
3668  if ( count( $existingAdded ) ) {
3669  $dbw->update(
3670  'category',
3671  $addFields,
3672  [ 'cat_title' => $existingAdded ],
3673  __METHOD__
3674  );
3675  }
3676 
3677  $missingAdded = array_diff( $added, $existingAdded );
3678  if ( count( $missingAdded ) ) {
3679  $insertRows = [];
3680  foreach ( $missingAdded as $cat ) {
3681  $insertRows[] = [
3682  'cat_title' => $cat,
3683  'cat_pages' => 1,
3684  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3685  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3686  ];
3687  }
3688  $dbw->upsert(
3689  'category',
3690  $insertRows,
3691  'cat_title',
3692  $addFields,
3693  __METHOD__
3694  );
3695  }
3696  }
3697 
3698  if ( count( $deleted ) ) {
3699  $dbw->update(
3700  'category',
3701  $removeFields,
3702  [ 'cat_title' => $deleted ],
3703  __METHOD__
3704  );
3705  }
3706 
3707  foreach ( $added as $catName ) {
3708  $cat = Category::newFromName( $catName );
3709  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3710  }
3711 
3712  foreach ( $deleted as $catName ) {
3713  $cat = Category::newFromName( $catName );
3714  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3715  // Refresh counts on categories that should be empty now (after commit, T166757)
3716  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3717  $cat->refreshCountsIfEmpty();
3718  } );
3719  }
3720  }
3721 
3728  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3729  if ( wfReadOnly() ) {
3730  return;
3731  }
3732 
3733  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3734  [ $this, $this->mTitle, $parserOutput ]
3735  ) ) {
3736  return;
3737  }
3738 
3739  $config = RequestContext::getMain()->getConfig();
3740 
3741  $params = [
3742  'isOpportunistic' => true,
3743  'rootJobTimestamp' => $parserOutput->getCacheTime()
3744  ];
3745 
3746  if ( $this->mTitle->areRestrictionsCascading() ) {
3747  // If the page is cascade protecting, the links should really be up-to-date
3748  JobQueueGroup::singleton()->lazyPush(
3749  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3750  );
3751  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3752  // Assume the output contains "dynamic" time/random based magic words.
3753  // Only update pages that expired due to dynamic content and NOT due to edits
3754  // to referenced templates/files. When the cache expires due to dynamic content,
3755  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3756  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3757  // template/file edit already triggered recursive RefreshLinksJob jobs.
3758  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3759  // If a page is uncacheable, do not keep spamming a job for it.
3760  // Although it would be de-duplicated, it would still waste I/O.
3762  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3763  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3764  if ( $cache->add( $key, time(), $ttl ) ) {
3765  JobQueueGroup::singleton()->lazyPush(
3766  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3767  );
3768  }
3769  }
3770  }
3771  }
3772 
3782  public function getDeletionUpdates( $rev = null ) {
3783  if ( !$rev ) {
3784  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3785 
3786  try {
3787  $rev = $this->getRevisionRecord();
3788  } catch ( Exception $ex ) {
3789  // If we can't load the content, something is wrong. Perhaps that's why
3790  // the user is trying to delete the page, so let's not fail in that case.
3791  // Note that doDeleteArticleReal() will already have logged an issue with
3792  // loading the content.
3793  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3794  }
3795  }
3796 
3797  if ( !$rev ) {
3798  $slotContent = [];
3799  } elseif ( $rev instanceof Content ) {
3800  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3801 
3802  $slotContent = [ SlotRecord::MAIN => $rev ];
3803  } else {
3804  $slotContent = array_map( function ( SlotRecord $slot ) {
3805  return $slot->getContent();
3806  }, $rev->getSlots()->getSlots() );
3807  }
3808 
3809  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3810 
3811  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3812  // model here, not the content object!
3813  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3815  foreach ( $slotContent as $role => $content ) {
3816  $handler = $content->getContentHandler();
3817 
3818  $updates = $handler->getDeletionUpdates(
3819  $this->getTitle(),
3820  $role
3821  );
3822  $allUpdates = array_merge( $allUpdates, $updates );
3823 
3824  // TODO: remove B/C hack in 1.32!
3825  $legacyUpdates = $content->getDeletionUpdates( $this );
3826 
3827  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3828  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3829  return !( $update instanceof LinksDeletionUpdate );
3830  } );
3831 
3832  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3833  }
3834 
3835  Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3836 
3837  // TODO: hard deprecate old hook in 1.33
3838  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3839  return $allUpdates;
3840  }
3841 
3849  public function isLocal() {
3850  return true;
3851  }
3852 
3862  public function getWikiDisplayName() {
3863  global $wgSitename;
3864  return $wgSitename;
3865  }
3866 
3875  public function getSourceURL() {
3876  return $this->getTitle()->getCanonicalURL();
3877  }
3878 
3885  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3886 
3887  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3888  }
3889 
3890 }
WikiPage\hasDifferencesOutsideMainSlot
static hasDifferencesOutsideMainSlot( $a, $b)
Helper method for checking whether two revisions have differences that go beyond the main slot.
Definition: WikiPage.php:1535
WikiPage\getCategories
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:3575
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:7000
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
Page
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:30
WikiPage\getComment
getComment( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:909
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:560
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:3404
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:458
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
WikiPage\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3629
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:769
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:1001
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:266
User\getId
getId()
Get the user's ID.
Definition: User.php:2159
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:326
Title\getFragment
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1741
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:2583
$wgPageCreationLog
$wgPageCreationLog
Maintain a log of page creations at Special:Log/create?
Definition: DefaultSettings.php:8230
WikiPage\updateRevisionOn
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1386
WikiPage\wasLoadedFrom
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:503
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:1562
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:137
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:145
WikiPage\getCreator
getCreator( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:855
EDIT_INTERNAL
const EDIT_INTERNAL
Definition: Defines.php:148
MediaWiki\Storage\DerivedPageDataUpdater\setArticleCountMethod
setArticleCountMethod( $articleCountMethod)
Definition: DerivedPageDataUpdater.php:431
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:78
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:1811
WikiPage\hasViewableContent
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition: WikiPage.php:596
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:663
WikiPage\doViewUpdates
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1276
MediaWiki\Storage\DerivedPageDataUpdater\setRcWatchCategoryMembership
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
Definition: DerivedPageDataUpdater.php:439
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1643
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1689
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:442
WikiPage\$mDataLoadedFrom
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:87
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:48
WikiPage\replaceSectionContent
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1605
PoolWorkArticleView
Definition: PoolWorkArticleView.php:28
NS_FILE
const NS_FILE
Definition: Defines.php:75
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1972
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1125
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1104
wfMsgReplaceArgs
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
Definition: GlobalFunctions.php:1233
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:536
WikiPage\getRevision
getRevision()
Get the latest revision.
Definition: WikiPage.php:757
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:1198
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:827
$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:1163
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:592
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1064
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
$success
$success
Definition: NoLocalSettings.php:42
User\pingLimiter
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1870
Message
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
Definition: FakeResultWrapper.php:11
WikiPage\archiveRevisions
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:2877
WikiPage\getSlotRoleRegistry
getSlotRoleRegistry()
Definition: WikiPage.php:254
WikiPage\onArticleEdit
static onArticleEdit(Title $title, Revision $revision=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3498
WikiPage\getDBLoadBalancer
getDBLoadBalancer()
Definition: WikiPage.php:275
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:285
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:7232
$revQuery
$revQuery
Definition: testCompression.php:56
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:7248
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:139
HTMLCacheUpdateJob\newForBacklinks
static newForBacklinks(Title $title, $table, $params=[])
Definition: HTMLCacheUpdateJob.php:59
WikiPage\$mTitle
Title $mTitle
Definition: WikiPage.php:54
WikiPage\$mTouched
string $mTouched
Definition: WikiPage.php:107
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:882
WikiPage\triggerOpportunisticLinksUpdate
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3728
WikiPage\protectDescription
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2515
$dbr
$dbr
Definition: testCompression.php:54
IDBAccessObject\READ_LOCKING
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:64
Revision
Definition: Revision.php:40
User\matchEditToken
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4407
WikiPage\updateParserCache
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2099
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1587
$wgEnableScaryTranscluding
$wgEnableScaryTranscluding
Enable interwiki transcluding.
Definition: DefaultSettings.php:4656
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:1025
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:143
WikiPage\doDeleteArticleBatched
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2705
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:931
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1030
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:3110
MediaWiki\Storage\DerivedPageDataUpdater\setLogger
setLogger(LoggerInterface $logger)
Definition: DerivedPageDataUpdater.php:318
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1034
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:527
wfIncrStats
wfIncrStats( $key, $count=1)
Increment a statistics counter.
Definition: GlobalFunctions.php:1094
WikiPage\doSecondaryDataUpdates
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2143
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2497
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:346
Title\getInterwiki
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:935
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1235
WikiPage\getId
getId()
Definition: WikiPage.php:571
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1341
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1213
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:307
MediaWiki\Content\ContentHandlerFactory
Definition: ContentHandlerFactory.php:42
WikiPage\exists
exists()
Definition: WikiPage.php:581
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:131
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3440
ObjectCache\getInstance
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:78
$args
if( $line===false) $args
Definition: mcc.php:124
WikiPage\$mRedirectTarget
Title $mRedirectTarget
Definition: WikiPage.php:92
WikiPage\__construct
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:123
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:652
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:70
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:674
WikiPage\purgeInterwikiCheckKey
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3547
WikiPage\$mDataLoaded
bool $mDataLoaded
Definition: WikiPage.php:60
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:63
$title
$title
Definition: testCompression.php:38
SiteStatsUpdate\factory
static factory(array $deltas)
Definition: SiteStatsUpdate.php:71
WikiPage\doDeleteUpdates
doDeleteUpdates( $id, Content $content=null, $revRecord=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:3022
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:595
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:815
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:685
ParserOptions\getStubThreshold
getStubThreshold()
Thumb size preferred by the user.
Definition: ParserOptions.php:551
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:617
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:72
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:1299
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:948
User\clearNotification
clearNotification(&$title, $oldid=0)
Clear the user's notification timestamp for the given title.
Definition: User.php:3709
WikiPage\getContentModel
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:623
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:428
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2993
Wikimedia\Rdbms\LoadBalancer
Database connection, tracking, load balancing, and transaction manager for a cluster.
Definition: LoadBalancer.php:42
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(Title $title, ?Revision $old, ?Revision $new, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:543
$wgPageLanguageUseDB
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
Definition: DefaultSettings.php:9040
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:1755
WikiPage\updateIfNewerOn
updateIfNewerOn( $dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record.
Definition: WikiPage.php:1497
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:72
WikiPage\getDeletionUpdates
getDeletionUpdates( $rev=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3782
WikiPage\protectDescriptionLog
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2557
WikiPage\insertRedirect
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1048
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:142
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:57
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
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:74
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:119
WikiPage\newFromID
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:181
WikiPage\insertProtectNullRevision
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2444
$wgSitename
$wgSitename
Name of the site.
Definition: DefaultSettings.php:81
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:3875
ParserOptions\newCanonical
static newCanonical( $context=null, $userLang=null)
Creates a "canonical" ParserOptions object.
Definition: ParserOptions.php:1059
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1485
WikiPage\$derivedDataUpdater
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:117
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:3157
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3598
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:211
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:82
$wgDeleteRevisionsBatchSize
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
Definition: DefaultSettings.php:5866
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:451
WikiPage\doEditContent
doEditContent(Content $content, $summary, $flags=0, $originalRevId=false, User $user=null, $serialFormat=null, $tags=[], $undidRevId=0)
Change an existing article or create a new article.
Definition: WikiPage.php:1893
WikiPage\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object.
Definition: WikiPage.php:359
WikiPage\getOldestRevision
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:698
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:707
WikiPage\setLastEdit
setLastEdit(RevisionRecord $revRecord)
Set the latest revision.
Definition: WikiPage.php:746
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1114
$context
$context
Definition: load.php:43
Content
Base interface for content objects.
Definition: Content.php:34
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:141
WikiPage\loadFromRow
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:529
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:5660
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2493
Title
Represents a title within MediaWiki.
Definition: Title.php:42
WikiPage\getUser
getUser( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:828
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:66
WikiPage\newDerivedDataUpdater
newDerivedDataUpdater()
Definition: WikiPage.php:1704
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:1117
$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:2071
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:330
$wgAjaxEditStash
$wgAjaxEditStash
Have clients send edits to be prepared when filling in edit summaries.
Definition: DefaultSettings.php:8548
WikiPage\$mId
int $mId
Definition: WikiPage.php:82
WikiPage\doDeleteArticleReal
doDeleteArticleReal( $reason, $user=false, $suppress=false, $u2=null, &$error='', User $deleter=null, $tags=[], $logsubtype='delete', $immediate=false)
Back-end article deletion Deletes the article with database consistency, writes logs,...
Definition: WikiPage.php:2650
DeletePageJob
Class DeletePageJob.
Definition: DeletePageJob.php:6
WikiPage\getWikiDisplayName
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3862
LinksUpdate\queueRecursiveJobsForTable
static queueRecursiveJobsForTable(Title $title, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
Definition: LinksUpdate.php:371
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:2002
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:223
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:3643
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3884
InfoAction\invalidateCache
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:71
$source
$source
Definition: mwdoc-filter.php:34
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:38
WikiPage\pageData
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:395
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:999
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:3849
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:1051
$wgArticleCountMethod
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article.
Definition: DefaultSettings.php:4699
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:77
Category\newFromName
static newFromName( $name)
Factory function.
Definition: Category.php:126
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:143
ParserOptions\isSafeToCache
isSafeToCache()
Test whether these options are safe to cache.
Definition: ParserOptions.php:1382
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:1074
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:801
WikiPage\getRevisionRenderer
getRevisionRenderer()
Definition: WikiPage.php:247
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1460
WikiPage\$mPreparedEdit
PreparedEdit false $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:77
Revision\getRevisionRecord
getRevisionRecord()
Definition: Revision.php:436
WikiPage\$mLinksUpdated
string $mLinksUpdated
Definition: WikiPage.php:112
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:2614
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:605
WikiPage\$mLastRevision
Revision $mLastRevision
Definition: WikiPage.php:97
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:102
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:60
CommentStore\getStore
static getStore()
Definition: CommentStore.php:116
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:54
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:125
ParserOutput\hasDynamicContent
hasDynamicContent()
Check whether the cache TTL was lowered due to dynamic content.
Definition: ParserOutput.php:1322
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:268
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:442
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:298
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:790
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2188
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:102
Title\purgeExpiredRestrictions
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3017
$wgDisableAnonTalk
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
Definition: DefaultSettings.php:7345
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:7222
WikiPage\getContentHandlerFactory
getContentHandlerFactory()
Definition: WikiPage.php:261
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:315
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:2173
WikiPage\getRevisionStore
getRevisionStore()
Definition: WikiPage.php:240
$type
$type
Definition: testCompression.php:52