MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
28 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
41 use Wikimedia\Assert\Assert;
42 use Wikimedia\IPUtils;
43 use Wikimedia\NonSerializable\NonSerializableTrait;
47 
54 class WikiPage implements Page, IDBAccessObject {
55  use NonSerializableTrait;
56  use ProtectedHookAccessorTrait;
57 
58  // Constants for $mDataLoadedFrom and related
59 
65  public $mTitle = null;
66 
72  public $mDataLoaded = false;
73 
79  public $mIsRedirect = false;
80 
86  public $mLatest = false;
87 
93  public $mPreparedEdit = false;
94 
98  protected $mId = null;
99 
104 
108  protected $mRedirectTarget = null;
109 
113  private $mLastRevision = null;
114 
118  protected $mTimestamp = '';
119 
123  protected $mTouched = '19700101000000';
124 
128  protected $mLinksUpdated = '19700101000000';
129 
133  private $derivedDataUpdater = null;
134 
138  public function __construct( Title $title ) {
139  $this->mTitle = $title;
140  }
141 
146  public function __clone() {
147  $this->mTitle = clone $this->mTitle;
148  }
149 
159  public static function factory( Title $title ) {
160  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
161  }
162 
174  public static function newFromID( $id, $from = 'fromdb' ) {
175  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromID( $id, $from );
176  }
177 
190  public static function newFromRow( $row, $from = 'fromdb' ) {
191  return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromRow( $row, $from );
192  }
193 
200  public static function convertSelectType( $type ) {
201  switch ( $type ) {
202  case 'fromdb':
203  return self::READ_NORMAL;
204  case 'fromdbmaster':
205  return self::READ_LATEST;
206  case 'forupdate':
207  return self::READ_LOCKING;
208  default:
209  // It may already be an integer or whatever else
210  return $type;
211  }
212  }
213 
217  private function getRevisionStore() {
218  return MediaWikiServices::getInstance()->getRevisionStore();
219  }
220 
224  private function getRevisionRenderer() {
225  return MediaWikiServices::getInstance()->getRevisionRenderer();
226  }
227 
231  private function getSlotRoleRegistry() {
232  return MediaWikiServices::getInstance()->getSlotRoleRegistry();
233  }
234 
239  return MediaWikiServices::getInstance()->getContentHandlerFactory();
240  }
241 
245  private function getParserCache() {
246  return MediaWikiServices::getInstance()->getParserCache();
247  }
248 
252  private function getDBLoadBalancer() {
253  return MediaWikiServices::getInstance()->getDBLoadBalancer();
254  }
255 
262  public function getActionOverrides() {
263  return $this->getContentHandler()->getActionOverrides();
264  }
265 
275  public function getContentHandler() {
276  return $this->getContentHandlerFactory()
277  ->getContentHandler( $this->getContentModel() );
278  }
279 
284  public function getTitle() {
285  return $this->mTitle;
286  }
287 
292  public function clear() {
293  $this->mDataLoaded = false;
294  $this->mDataLoadedFrom = self::READ_NONE;
295 
296  $this->clearCacheFields();
297  }
298 
303  protected function clearCacheFields() {
304  $this->mId = null;
305  $this->mRedirectTarget = null; // Title object if set
306  $this->mLastRevision = null; // Latest revision
307  $this->mTouched = '19700101000000';
308  $this->mLinksUpdated = '19700101000000';
309  $this->mTimestamp = '';
310  $this->mIsRedirect = false;
311  $this->mLatest = false;
312  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
313  // checks the requested rev ID and content against the cached one. For most
314  // content types, the output should not change during the lifetime of this cache.
315  // Clearing it can cause extra parses on edit for no reason.
316  }
317 
323  public function clearPreparedEdit() {
324  $this->mPreparedEdit = false;
325  }
326 
336  public static function getQueryInfo() {
337  global $wgPageLanguageUseDB;
338 
339  $ret = [
340  'tables' => [ 'page' ],
341  'fields' => [
342  'page_id',
343  'page_namespace',
344  'page_title',
345  'page_restrictions',
346  'page_is_redirect',
347  'page_is_new',
348  'page_random',
349  'page_touched',
350  'page_links_updated',
351  'page_latest',
352  'page_len',
353  'page_content_model',
354  ],
355  'joins' => [],
356  ];
357 
358  if ( $wgPageLanguageUseDB ) {
359  $ret['fields'][] = 'page_lang';
360  }
361 
362  return $ret;
363  }
364 
372  protected function pageData( $dbr, $conditions, $options = [] ) {
373  $pageQuery = self::getQueryInfo();
374 
375  $this->getHookRunner()->onArticlePageDataBefore(
376  $this, $pageQuery['fields'], $pageQuery['tables'], $pageQuery['joins'] );
377 
378  $row = $dbr->selectRow(
379  $pageQuery['tables'],
380  $pageQuery['fields'],
381  $conditions,
382  __METHOD__,
383  $options,
384  $pageQuery['joins']
385  );
386 
387  $this->getHookRunner()->onArticlePageDataAfter( $this, $row );
388 
389  return $row;
390  }
391 
401  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
402  return $this->pageData( $dbr, [
403  'page_namespace' => $title->getNamespace(),
404  'page_title' => $title->getDBkey() ], $options );
405  }
406 
415  public function pageDataFromId( $dbr, $id, $options = [] ) {
416  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
417  }
418 
431  public function loadPageData( $from = 'fromdb' ) {
432  $from = self::convertSelectType( $from );
433  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
434  // We already have the data from the correct location, no need to load it twice.
435  return;
436  }
437 
438  if ( is_int( $from ) ) {
439  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
440  $loadBalancer = $this->getDBLoadBalancer();
441  $db = $loadBalancer->getConnection( $index );
442  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
443 
444  if ( !$data
445  && $index == DB_REPLICA
446  && $loadBalancer->getServerCount() > 1
447  && $loadBalancer->hasOrMadeRecentMasterChanges()
448  ) {
449  $from = self::READ_LATEST;
450  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
451  $db = $loadBalancer->getConnection( $index );
452  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
453  }
454  } else {
455  // No idea from where the caller got this data, assume replica DB.
456  $data = $from;
457  $from = self::READ_NORMAL;
458  }
459 
460  $this->loadFromRow( $data, $from );
461  }
462 
476  public function wasLoadedFrom( $from ) {
477  $from = self::convertSelectType( $from );
478 
479  if ( !is_int( $from ) ) {
480  // No idea from where the caller got this data, assume replica DB.
481  $from = self::READ_NORMAL;
482  }
483 
484  if ( $from <= $this->mDataLoadedFrom ) {
485  return true;
486  }
487 
488  return false;
489  }
490 
502  public function loadFromRow( $data, $from ) {
503  $lc = MediaWikiServices::getInstance()->getLinkCache();
504  $lc->clearLink( $this->mTitle );
505 
506  if ( $data ) {
507  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
508 
509  $this->mTitle->loadFromRow( $data );
510 
511  // Old-fashioned restrictions
512  $this->mTitle->loadRestrictions( $data->page_restrictions );
513 
514  $this->mId = intval( $data->page_id );
515  $this->mTouched = MWTimestamp::convert( TS_MW, $data->page_touched );
516  $this->mLinksUpdated = $data->page_links_updated === null
517  ? null
518  : MWTimestamp::convert( TS_MW, $data->page_links_updated );
519  $this->mIsRedirect = intval( $data->page_is_redirect );
520  $this->mLatest = intval( $data->page_latest );
521  // T39225: $latest may no longer match the cached latest RevisionRecord object.
522  // Double-check the ID of any cached latest RevisionRecord object for consistency.
523  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
524  $this->mLastRevision = null;
525  $this->mTimestamp = '';
526  }
527  } else {
528  $lc->addBadLinkObj( $this->mTitle );
529 
530  $this->mTitle->loadFromRow( false );
531 
532  $this->clearCacheFields();
533 
534  $this->mId = 0;
535  }
536 
537  $this->mDataLoaded = true;
538  $this->mDataLoadedFrom = self::convertSelectType( $from );
539  }
540 
544  public function getId() {
545  if ( !$this->mDataLoaded ) {
546  $this->loadPageData();
547  }
548  return $this->mId;
549  }
550 
554  public function exists() {
555  if ( !$this->mDataLoaded ) {
556  $this->loadPageData();
557  }
558  return $this->mId > 0;
559  }
560 
569  public function hasViewableContent() {
570  return $this->mTitle->isKnown();
571  }
572 
578  public function isRedirect() {
579  if ( !$this->mDataLoaded ) {
580  $this->loadPageData();
581  }
582 
583  return (bool)$this->mIsRedirect;
584  }
585 
596  public function getContentModel() {
597  if ( $this->exists() ) {
598  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
599 
600  return $cache->getWithSetCallback(
601  $cache->makeKey( 'page-content-model', $this->getLatest() ),
602  $cache::TTL_MONTH,
603  function () {
604  $rev = $this->getRevisionRecord();
605  if ( $rev ) {
606  // Look at the revision's actual content model
607  $slot = $rev->getSlot(
608  SlotRecord::MAIN,
609  RevisionRecord::RAW
610  );
611  return $slot->getModel();
612  } else {
613  $title = $this->mTitle->getPrefixedDBkey();
614  wfWarn( "Page $title exists but has no (visible) revisions!" );
615  return $this->mTitle->getContentModel();
616  }
617  }
618  );
619  }
620 
621  // use the default model for this page
622  return $this->mTitle->getContentModel();
623  }
624 
629  public function checkTouched() {
630  if ( !$this->mDataLoaded ) {
631  $this->loadPageData();
632  }
633  return ( $this->mId && !$this->mIsRedirect );
634  }
635 
640  public function getTouched() {
641  if ( !$this->mDataLoaded ) {
642  $this->loadPageData();
643  }
644  return $this->mTouched;
645  }
646 
651  public function getLinksTimestamp() {
652  if ( !$this->mDataLoaded ) {
653  $this->loadPageData();
654  }
655  return $this->mLinksUpdated;
656  }
657 
662  public function getLatest() {
663  if ( !$this->mDataLoaded ) {
664  $this->loadPageData();
665  }
666  return (int)$this->mLatest;
667  }
668 
675  public function getOldestRevision() {
676  wfDeprecated( __METHOD__, '1.35' );
677  $rev = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
678  return $rev ? new Revision( $rev ) : null;
679  }
680 
685  protected function loadLastEdit() {
686  if ( $this->mLastRevision !== null ) {
687  return; // already loaded
688  }
689 
690  $latest = $this->getLatest();
691  if ( !$latest ) {
692  return; // page doesn't exist or is missing page_latest info
693  }
694 
695  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
696  // T39225: if session S1 loads the page row FOR UPDATE, the result always
697  // includes the latest changes committed. This is true even within REPEATABLE-READ
698  // transactions, where S1 normally only sees changes committed before the first S1
699  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
700  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
701  // happened after the first S1 SELECT.
702  // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
703  $revision = $this->getRevisionStore()
704  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LOCKING );
705  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
706  // Bug T93976: if page_latest was loaded from the master, fetch the
707  // revision from there as well, as it may not exist yet on a replica DB.
708  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
709  $revision = $this->getRevisionStore()
710  ->getRevisionByPageId( $this->getId(), $latest, RevisionStore::READ_LATEST );
711  } else {
712  $revision = $this->getRevisionStore()->getKnownCurrentRevision( $this->getTitle(), $latest );
713  }
714 
715  if ( $revision ) { // sanity
716  $this->setLastEdit( $revision );
717  }
718  }
719 
724  private function setLastEdit( RevisionRecord $revRecord ) {
725  $this->mLastRevision = $revRecord;
726  $this->mTimestamp = $revRecord->getTimestamp();
727  }
728 
734  public function getRevision() {
735  wfDeprecated( __METHOD__, '1.35' );
736  $this->loadLastEdit();
737  if ( $this->mLastRevision ) {
738  return new Revision( $this->mLastRevision );
739  }
740  return null;
741  }
742 
748  public function getRevisionRecord() {
749  $this->loadLastEdit();
750  if ( $this->mLastRevision ) {
751  return $this->mLastRevision;
752  }
753  return null;
754  }
755 
769  public function getContent( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
770  $this->loadLastEdit();
771  if ( $this->mLastRevision ) {
772  return $this->mLastRevision->getContent( SlotRecord::MAIN, $audience, $user );
773  }
774  return null;
775  }
776 
780  public function getTimestamp() {
781  // Check if the field has been filled by WikiPage::setTimestamp()
782  if ( !$this->mTimestamp ) {
783  $this->loadLastEdit();
784  }
785 
786  return MWTimestamp::convert( TS_MW, $this->mTimestamp );
787  }
788 
794  public function setTimestamp( $ts ) {
795  $this->mTimestamp = MWTimestamp::convert( TS_MW, $ts );
796  }
797 
808  public function getUser( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
809  $this->loadLastEdit();
810  if ( $this->mLastRevision ) {
811  $revUser = $this->mLastRevision->getUser( $audience, $user );
812  return $revUser ? $revUser->getId() : 0;
813  } else {
814  return -1;
815  }
816  }
817 
829  public function getCreator( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
830  $revRecord = $this->getRevisionStore()->getFirstRevision( $this->getTitle() );
831  if ( $revRecord ) {
832  return $revRecord->getUser( $audience, $user );
833  } else {
834  return null;
835  }
836  }
837 
848  public function getUserText( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
849  $this->loadLastEdit();
850  if ( $this->mLastRevision ) {
851  $revUser = $this->mLastRevision->getUser( $audience, $user );
852  return $revUser ? $revUser->getName() : '';
853  } else {
854  return '';
855  }
856  }
857 
869  public function getComment( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
870  $this->loadLastEdit();
871  if ( $this->mLastRevision ) {
872  $revComment = $this->mLastRevision->getComment( $audience, $user );
873  return $revComment ? $revComment->text : '';
874  } else {
875  return '';
876  }
877  }
878 
884  public function getMinorEdit() {
885  $this->loadLastEdit();
886  if ( $this->mLastRevision ) {
887  return $this->mLastRevision->isMinor();
888  } else {
889  return false;
890  }
891  }
892 
901  public function isCountable( $editInfo = false ) {
902  global $wgArticleCountMethod;
903 
904  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
905 
906  if ( !$this->mTitle->isContentPage() ) {
907  return false;
908  }
909 
910  if ( $editInfo ) {
911  // NOTE: only the main slot can make a page a redirect
912  $content = $editInfo->pstContent;
913  } else {
914  $content = $this->getContent();
915  }
916 
917  if ( !$content || $content->isRedirect() ) {
918  return false;
919  }
920 
921  $hasLinks = null;
922 
923  if ( $wgArticleCountMethod === 'link' ) {
924  // nasty special case to avoid re-parsing to detect links
925 
926  if ( $editInfo ) {
927  // ParserOutput::getLinks() is a 2D array of page links, so
928  // to be really correct we would need to recurse in the array
929  // but the main array should only have items in it if there are
930  // links.
931  $hasLinks = (bool)count( $editInfo->output->getLinks() );
932  } else {
933  // NOTE: keep in sync with RevisionRenderer::getLinkCount
934  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
935  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', '1',
936  [ 'pl_from' => $this->getId() ], __METHOD__ );
937  }
938  }
939 
940  // TODO: MCR: determine $hasLinks for each slot, and use that info
941  // with that slot's Content's isCountable method. That requires per-
942  // slot ParserOutput in the ParserCache, or per-slot info in the
943  // pagelinks table.
944  return $content->isCountable( $hasLinks );
945  }
946 
954  public function getRedirectTarget() {
955  if ( !$this->mTitle->isRedirect() ) {
956  return null;
957  }
958 
959  if ( $this->mRedirectTarget !== null ) {
960  return $this->mRedirectTarget;
961  }
962 
963  // Query the redirect table
964  $dbr = wfGetDB( DB_REPLICA );
965  $row = $dbr->selectRow( 'redirect',
966  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
967  [ 'rd_from' => $this->getId() ],
968  __METHOD__
969  );
970 
971  // rd_fragment and rd_interwiki were added later, populate them if empty
972  if ( $row && $row->rd_fragment !== null && $row->rd_interwiki !== null ) {
973  // (T203942) We can't redirect to Media namespace because it's virtual.
974  // We don't want to modify Title objects farther down the
975  // line. So, let's fix this here by changing to File namespace.
976  if ( $row->rd_namespace == NS_MEDIA ) {
977  $namespace = NS_FILE;
978  } else {
979  $namespace = $row->rd_namespace;
980  }
981  $this->mRedirectTarget = Title::makeTitle(
982  $namespace, $row->rd_title,
983  $row->rd_fragment, $row->rd_interwiki
984  );
985  return $this->mRedirectTarget;
986  }
987 
988  // This page doesn't have an entry in the redirect table
989  $this->mRedirectTarget = $this->insertRedirect();
990  return $this->mRedirectTarget;
991  }
992 
1001  public function insertRedirect() {
1002  $content = $this->getContent();
1003  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1004  if ( !$retval ) {
1005  return null;
1006  }
1007 
1008  // Update the DB post-send if the page has not cached since now
1009  $latest = $this->getLatest();
1011  function () use ( $retval, $latest ) {
1012  $this->insertRedirectEntry( $retval, $latest );
1013  },
1015  wfGetDB( DB_MASTER )
1016  );
1017 
1018  return $retval;
1019  }
1020 
1027  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1028  $dbw = wfGetDB( DB_MASTER );
1029  $dbw->startAtomic( __METHOD__ );
1030 
1031  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1032  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1033  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1034  $dbw->upsert(
1035  'redirect',
1036  [
1037  'rd_from' => $this->getId(),
1038  'rd_namespace' => $rt->getNamespace(),
1039  'rd_title' => $rt->getDBkey(),
1040  'rd_fragment' => $truncatedFragment,
1041  'rd_interwiki' => $rt->getInterwiki(),
1042  ],
1043  'rd_from',
1044  [
1045  'rd_namespace' => $rt->getNamespace(),
1046  'rd_title' => $rt->getDBkey(),
1047  'rd_fragment' => $truncatedFragment,
1048  'rd_interwiki' => $rt->getInterwiki(),
1049  ],
1050  __METHOD__
1051  );
1052  $success = true;
1053  } else {
1054  $success = false;
1055  }
1056 
1057  $dbw->endAtomic( __METHOD__ );
1058 
1059  return $success;
1060  }
1061 
1067  public function followRedirect() {
1068  return $this->getRedirectURL( $this->getRedirectTarget() );
1069  }
1070 
1078  public function getRedirectURL( $rt ) {
1079  if ( !$rt ) {
1080  return false;
1081  }
1082 
1083  if ( $rt->isExternal() ) {
1084  if ( $rt->isLocal() ) {
1085  // Offsite wikis need an HTTP redirect.
1086  // This can be hard to reverse and may produce loops,
1087  // so they may be disabled in the site configuration.
1088  $source = $this->mTitle->getFullURL( 'redirect=no' );
1089  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1090  } else {
1091  // External pages without "local" bit set are not valid
1092  // redirect targets
1093  return false;
1094  }
1095  }
1096 
1097  if ( $rt->isSpecialPage() ) {
1098  // Gotta handle redirects to special pages differently:
1099  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1100  // Some pages are not valid targets.
1101  if ( $rt->isValidRedirectTarget() ) {
1102  return $rt->getFullURL();
1103  } else {
1104  return false;
1105  }
1106  }
1107 
1108  return $rt;
1109  }
1110 
1116  public function getContributors() {
1117  // @todo: This is expensive; cache this info somewhere.
1118 
1119  $dbr = wfGetDB( DB_REPLICA );
1120 
1121  $actorMigration = ActorMigration::newMigration();
1122  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1123 
1124  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1125 
1126  $revactor_actor = $actorQuery['fields']['rev_actor'];
1127  $fields = [
1128  'user_id' => $actorQuery['fields']['rev_user'],
1129  'user_name' => $actorQuery['fields']['rev_user_text'],
1130  'actor_id' => "MIN($revactor_actor)",
1131  'user_real_name' => 'MIN(user_real_name)',
1132  'timestamp' => 'MAX(rev_timestamp)',
1133  ];
1134 
1135  $conds = [ 'rev_page' => $this->getId() ];
1136 
1137  // The user who made the top revision gets credited as "this page was last edited by
1138  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1139  $user = $this->getUser()
1140  ? User::newFromId( $this->getUser() )
1141  : User::newFromName( $this->getUserText(), false );
1142  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1143 
1144  // Username hidden?
1145  $conds[] = "{$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER )} = 0";
1146 
1147  $jconds = [
1148  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1149  ] + $actorQuery['joins'];
1150 
1151  $options = [
1152  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1153  'ORDER BY' => 'timestamp DESC',
1154  ];
1155 
1156  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1157  return new UserArrayFromResult( $res );
1158  }
1159 
1167  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1168  return $parserOptions->getStubThreshold() == 0
1169  && $this->exists()
1170  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1171  && $this->getContentHandler()->isParserCacheSupported();
1172  }
1173 
1189  public function getParserOutput(
1190  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1191  ) {
1192  $useParserCache =
1193  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1194 
1195  if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1196  throw new InvalidArgumentException(
1197  'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1198  );
1199  }
1200 
1201  wfDebug( __METHOD__ .
1202  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) );
1203  if ( $parserOptions->getStubThreshold() ) {
1204  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1205  $stats->updateCount( 'pcache.miss.stub', 1 );
1206  }
1207 
1208  if ( $useParserCache ) {
1209  $parserOutput = $this->getParserCache()
1210  ->get( $this, $parserOptions );
1211  if ( $parserOutput !== false ) {
1212  return $parserOutput;
1213  }
1214  }
1215 
1216  if ( $oldid === null || $oldid === 0 ) {
1217  $oldid = $this->getLatest();
1218  }
1219 
1220  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1221  $pool->execute();
1222 
1223  return $pool->getParserOutput();
1224  }
1225 
1231  public function doViewUpdates( User $user, $oldid = 0 ) {
1232  if ( wfReadOnly() ) {
1233  return;
1234  }
1235 
1236  // Update newtalk / watchlist notification status;
1237  // Avoid outage if the master is not reachable by using a deferred updated
1239  function () use ( $user, $oldid ) {
1240  $this->getHookRunner()->onPageViewUpdates( $this, $user );
1241 
1242  $user->clearNotification( $this->mTitle, $oldid );
1243  },
1245  );
1246  }
1247 
1254  public function doPurge() {
1255  if ( !$this->getHookRunner()->onArticlePurge( $this ) ) {
1256  return false;
1257  }
1258 
1259  $this->mTitle->invalidateCache();
1260 
1261  // Clear file cache and send purge after above page_touched update was committed
1262  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1263  $hcu->purgeTitleUrls( $this->mTitle, $hcu::PURGE_PRESEND );
1264 
1265  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) {
1266  MediaWikiServices::getInstance()->getMessageCache()
1267  ->updateMessageOverride( $this->mTitle, $this->getContent() );
1268  }
1269 
1270  return true;
1271  }
1272 
1289  public function insertOn( $dbw, $pageId = null ) {
1290  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1291  $dbw->insert(
1292  'page',
1293  [
1294  'page_namespace' => $this->mTitle->getNamespace(),
1295  'page_title' => $this->mTitle->getDBkey(),
1296  'page_restrictions' => '',
1297  'page_is_redirect' => 0, // Will set this shortly...
1298  'page_is_new' => 1,
1299  'page_random' => wfRandom(),
1300  'page_touched' => $dbw->timestamp(),
1301  'page_latest' => 0, // Fill this in shortly...
1302  'page_len' => 0, // Fill this in shortly...
1303  ] + $pageIdForInsert,
1304  __METHOD__,
1305  [ 'IGNORE' ]
1306  );
1307 
1308  if ( $dbw->affectedRows() > 0 ) {
1309  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1310  $this->mId = $newid;
1311  $this->mTitle->resetArticleID( $newid );
1312 
1313  return $newid;
1314  } else {
1315  return false; // nothing changed
1316  }
1317  }
1318 
1334  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1335  $lastRevIsRedirect = null
1336  ) {
1337  // TODO: move into PageUpdater or PageStore
1338  // NOTE: when doing that, make sure cached fields get reset in doEditContent,
1339  // and in the compat stub!
1340 
1341  // Assertion to try to catch T92046
1342  if ( (int)$revision->getId() === 0 ) {
1343  throw new InvalidArgumentException(
1344  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1345  );
1346  }
1347 
1348  if ( $revision instanceof Revision ) {
1349  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
1350  $revision = $revision->getRevisionRecord();
1351  }
1352 
1353  $content = $revision->getContent( SlotRecord::MAIN );
1354  $len = $content ? $content->getSize() : 0;
1355  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1356 
1357  $conditions = [ 'page_id' => $this->getId() ];
1358 
1359  if ( $lastRevision !== null ) {
1360  // An extra check against threads stepping on each other
1361  $conditions['page_latest'] = $lastRevision;
1362  }
1363 
1364  $revId = $revision->getId();
1365  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1366 
1367  $model = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel();
1368 
1369  $row = [ /* SET */
1370  'page_latest' => $revId,
1371  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1372  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1373  'page_is_redirect' => $rt !== null ? 1 : 0,
1374  'page_len' => $len,
1375  'page_content_model' => $model,
1376  ];
1377 
1378  $dbw->update( 'page',
1379  $row,
1380  $conditions,
1381  __METHOD__ );
1382 
1383  $result = $dbw->affectedRows() > 0;
1384  if ( $result ) {
1385  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1386  $this->setLastEdit( $revision );
1387  $this->mLatest = $revision->getId();
1388  $this->mIsRedirect = (bool)$rt;
1389  // Update the LinkCache.
1390  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1391  $linkCache->addGoodLinkObj(
1392  $this->getId(),
1393  $this->mTitle,
1394  $len,
1395  $this->mIsRedirect,
1396  $this->mLatest,
1397  $model
1398  );
1399  }
1400 
1401  return $result;
1402  }
1403 
1415  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1416  // Always update redirects (target link might have changed)
1417  // Update/Insert if we don't know if the last revision was a redirect or not
1418  // Delete if changing from redirect to non-redirect
1419  $isRedirect = $redirectTitle !== null;
1420 
1421  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1422  return true;
1423  }
1424 
1425  if ( $isRedirect ) {
1426  $success = $this->insertRedirectEntry( $redirectTitle );
1427  } else {
1428  // This is not a redirect, remove row from redirect table
1429  $where = [ 'rd_from' => $this->getId() ];
1430  $dbw->delete( 'redirect', $where, __METHOD__ );
1431  $success = true;
1432  }
1433 
1434  if ( $this->getTitle()->getNamespace() === NS_FILE ) {
1435  MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
1436  ->invalidateImageRedirect( $this->getTitle() );
1437  }
1438 
1439  return $success;
1440  }
1441 
1452  public function updateIfNewerOn( $dbw, $revision ) {
1453  wfDeprecated( __METHOD__, '1.24' );
1454 
1455  $revisionRecord = $revision->getRevisionRecord();
1456 
1457  $row = $dbw->selectRow(
1458  [ 'revision', 'page' ],
1459  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1460  [
1461  'page_id' => $this->getId(),
1462  'page_latest=rev_id'
1463  ],
1464  __METHOD__
1465  );
1466 
1467  if ( $row ) {
1468  $rowTimestamp = MWTimestamp::convert( TS_MW, $row->rev_timestamp );
1469  if ( $rowTimestamp >= $revisionRecord->getTimestamp() ) {
1470  return false;
1471  }
1472  $prev = $row->rev_id;
1473  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1474  } else {
1475  // No or missing previous revision; mark the page as new
1476  $prev = 0;
1477  $lastRevIsRedirect = null;
1478  }
1479 
1480  $ret = $this->updateRevisionOn(
1481  $dbw,
1482  $revisionRecord,
1483  $prev,
1484  $lastRevIsRedirect
1485  );
1486 
1487  return $ret;
1488  }
1489 
1502  public static function hasDifferencesOutsideMainSlot( $a, $b ) {
1503  if ( $a instanceof Revision ) {
1504  wfDeprecated( __METHOD__ . ' with Revision objects', '1.35' );
1505  $a = $a->getRevisionRecord();
1506  }
1507  if ( $b instanceof Revision ) {
1508  wfDeprecated( __METHOD__ . ' with Revision objects', '1.35' );
1509  $b = $b->getRevisionRecord();
1510  }
1511  $aSlots = $a->getSlots();
1512  $bSlots = $b->getSlots();
1513  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1514 
1515  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1516  }
1517 
1531  public function getUndoContent( Revision $undo, Revision $undoafter ) {
1532  wfDeprecated( __METHOD__, '1.35' );
1533  // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1534 
1535  if ( self::hasDifferencesOutsideMainSlot(
1536  $undo->getRevisionRecord(),
1537  $undoafter->getRevisionRecord()
1538  ) ) {
1539  // Cannot yet undo edits that involve anything other the main slot.
1540  return false;
1541  }
1542 
1543  $handler = $undo->getContentHandler();
1544 
1545  // TODO remove use of Revision objects by deprecating this method entirely
1546  $revRecord = $this->getRevisionRecord();
1547  $revision = $revRecord ? new Revision( $revRecord ) : null;
1548  return $handler->getUndoContent( $revision, $undo, $undoafter );
1549  }
1550 
1561  public function supportsSections() {
1562  return $this->getContentHandler()->supportsSections();
1563  }
1564 
1579  public function replaceSectionContent(
1580  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1581  ) {
1582  $baseRevId = null;
1583  if ( $edittime && $sectionId !== 'new' ) {
1584  $lb = $this->getDBLoadBalancer();
1585  $rev = $this->getRevisionStore()->getRevisionByTimestamp( $this->mTitle, $edittime );
1586  // Try the master if this thread may have just added it.
1587  // This could be abstracted into a Revision method, but we don't want
1588  // to encourage loading of revisions by timestamp.
1589  if ( !$rev
1590  && $lb->getServerCount() > 1
1591  && $lb->hasOrMadeRecentMasterChanges()
1592  ) {
1593  $rev = $this->getRevisionStore()->getRevisionByTimestamp(
1594  $this->mTitle, $edittime, RevisionStore::READ_LATEST );
1595  }
1596  if ( $rev ) {
1597  $baseRevId = $rev->getId();
1598  }
1599  }
1600 
1601  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1602  }
1603 
1617  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1618  $sectionTitle = '', $baseRevId = null
1619  ) {
1620  if ( strval( $sectionId ) === '' ) {
1621  // Whole-page edit; let the whole text through
1622  $newContent = $sectionContent;
1623  } else {
1624  if ( !$this->supportsSections() ) {
1625  throw new MWException( "sections not supported for content model " .
1626  $this->getContentHandler()->getModelID() );
1627  }
1628 
1629  // T32711: always use current version when adding a new section
1630  if ( $baseRevId === null || $sectionId === 'new' ) {
1631  $oldContent = $this->getContent();
1632  } else {
1633  $revRecord = $this->getRevisionStore()->getRevisionById( $baseRevId );
1634  if ( !$revRecord ) {
1635  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1636  $this->getId() . "; section: $sectionId)" );
1637  return null;
1638  }
1639 
1640  $oldContent = $revRecord->getContent( SlotRecord::MAIN );
1641  }
1642 
1643  if ( !$oldContent ) {
1644  wfDebug( __METHOD__ . ": no page text" );
1645  return null;
1646  }
1647 
1648  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1649  }
1650 
1651  return $newContent;
1652  }
1653 
1663  public function checkFlags( $flags ) {
1664  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1665  if ( $this->exists() ) {
1666  $flags |= EDIT_UPDATE;
1667  } else {
1668  $flags |= EDIT_NEW;
1669  }
1670  }
1671 
1672  return $flags;
1673  }
1674 
1678  private function newDerivedDataUpdater() {
1680 
1681  $services = MediaWikiServices::getInstance();
1682  $editResultCache = new EditResultCache(
1683  $services->getMainObjectStash(),
1684  $services->getDBLoadBalancer(),
1685  new ServiceOptions(
1686  EditResultCache::CONSTRUCTOR_OPTIONS,
1687  $services->getMainConfig()
1688  )
1689  );
1690 
1692  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1693  $this->getRevisionStore(),
1694  $this->getRevisionRenderer(),
1695  $this->getSlotRoleRegistry(),
1696  $this->getParserCache(),
1698  $services->getMessageCache(),
1699  $services->getContentLanguage(),
1700  $services->getDBLoadBalancerFactory(),
1701  $this->getContentHandlerFactory(),
1702  $this->getHookContainer(),
1703  $editResultCache
1704  );
1705 
1706  $derivedDataUpdater->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
1709 
1710  return $derivedDataUpdater;
1711  }
1712 
1740  private function getDerivedDataUpdater(
1741  User $forUser = null,
1742  RevisionRecord $forRevision = null,
1743  RevisionSlotsUpdate $forUpdate = null,
1744  $forEdit = false
1745  ) {
1746  if ( !$forRevision && !$forUpdate ) {
1747  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1748  // going to use it with.
1749  $this->derivedDataUpdater = null;
1750  }
1751 
1752  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1753  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1754  // to it did not yet initialize it, because we don't know what data it will be
1755  // initialized with.
1756  $this->derivedDataUpdater = null;
1757  }
1758 
1759  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1760  // However, there is no good way to construct a cache key. We'd need to check against all
1761  // cached instances.
1762 
1763  if ( $this->derivedDataUpdater
1764  && !$this->derivedDataUpdater->isReusableFor(
1765  $forUser,
1766  $forRevision,
1767  $forUpdate,
1768  $forEdit ? $this->getLatest() : null
1769  )
1770  ) {
1771  $this->derivedDataUpdater = null;
1772  }
1773 
1774  if ( !$this->derivedDataUpdater ) {
1775  $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1776  }
1777 
1779  }
1780 
1796  public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1797  $config = MediaWikiServices::getInstance()->getMainConfig();
1798 
1799  $pageUpdater = new PageUpdater(
1800  $user,
1801  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1802  $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1803  $this->getDBLoadBalancer(),
1804  $this->getRevisionStore(),
1805  $this->getSlotRoleRegistry(),
1806  $this->getContentHandlerFactory(),
1807  $this->getHookContainer(),
1808  new ServiceOptions(
1809  PageUpdater::CONSTRUCTOR_OPTIONS,
1810  $config
1811  ),
1813  );
1814 
1815  $pageUpdater->setUsePageCreationLog( $config->get( 'PageCreationLog' ) );
1816  $pageUpdater->setAjaxEditStash( $config->get( 'AjaxEditStash' ) );
1817  $pageUpdater->setUseAutomaticEditSummaries(
1818  $config->get( 'UseAutomaticEditSummaries' )
1819  );
1820 
1821  return $pageUpdater;
1822  }
1823 
1890  public function doEditContent(
1891  Content $content, $summary, $flags = 0, $originalRevId = false,
1892  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1893  ) {
1894  global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1895 
1896  if ( !( $summary instanceof CommentStoreComment ) ) {
1897  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1898  }
1899 
1900  if ( !$user ) {
1901  $user = $wgUser;
1902  }
1903 
1904  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1905  // Checking the minoredit right should be done in the same place the 'bot' right is
1906  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1907  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1908  if ( ( $flags & EDIT_MINOR ) && !$permissionManager->userHasRight( $user, 'minoredit' ) ) {
1909  $flags &= ~EDIT_MINOR;
1910  }
1911 
1912  $slotsUpdate = new RevisionSlotsUpdate();
1913  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1914 
1915  // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1916  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1917  // used by this PageUpdater. However, there is no guarantee for this.
1918  $updater = $this->newPageUpdater( $user, $slotsUpdate );
1919  $updater->setContent( SlotRecord::MAIN, $content );
1920 
1921  $revisionStore = $this->getRevisionStore();
1922  $originalRevision = $originalRevId ? $revisionStore->getRevisionById( $originalRevId ) : null;
1923  if ( $originalRevision && $undidRevId !== 0 ) {
1924  // Mark it as a revert if it's an undo
1925  $oldestRevertedRev = $revisionStore->getNextRevision( $originalRevision );
1926  if ( $oldestRevertedRev ) {
1927  $updater->markAsRevert(
1928  EditResult::REVERT_UNDO,
1929  $oldestRevertedRev->getId(),
1930  $undidRevId
1931  );
1932  } else {
1933  // We can't find the oldest reverted revision for some reason
1934  $updater->markAsRevert( EditResult::REVERT_UNDO, $undidRevId );
1935  }
1936  } elseif ( $undidRevId !== 0 ) {
1937  // It's an undo, but the original revision is not specified, fall back to just
1938  // marking it as an undo with one revision undone.
1939  $updater->markAsRevert( EditResult::REVERT_UNDO, $undidRevId );
1940  // Try finding the original revision ID by assuming it's the one before the edit
1941  // that is being undone. If the bet fails, $originalRevision is ignored anyway, so
1942  // no damage is done.
1943  $undidRevision = $revisionStore->getRevisionById( $undidRevId );
1944  if ( $undidRevision ) {
1945  $originalRevision = $revisionStore->getPreviousRevision( $undidRevision );
1946  }
1947  }
1948 
1949  // Make sure original revision's content is the same as the new content and save the
1950  // original revision ID.
1951  if ( $originalRevision &&
1952  $originalRevision->getContent( SlotRecord::MAIN )->equals( $content )
1953  ) {
1954  $updater->setOriginalRevisionId( $originalRevision->getId() );
1955  }
1956 
1957  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1958 
1959  // TODO: this logic should not be in the storage layer, it's here for compatibility
1960  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1961  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1962 
1963  if ( $needsPatrol && $permissionManager->userCan(
1964  'autopatrol', $user, $this->getTitle()
1965  ) ) {
1966  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1967  }
1968 
1969  $updater->addTags( $tags );
1970 
1971  $revRec = $updater->saveRevision(
1972  $summary,
1973  $flags
1974  );
1975 
1976  // $revRec will be null if the edit failed, or if no new revision was created because
1977  // the content did not change.
1978  if ( $revRec ) {
1979  // update cached fields
1980  // TODO: this is currently redundant to what is done in updateRevisionOn.
1981  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1982  $this->setLastEdit( $revRec );
1983  $this->mLatest = $revRec->getId();
1984  }
1985 
1986  return $updater->getStatus();
1987  }
1988 
2003  public function makeParserOptions( $context ) {
2004  $options = ParserOptions::newCanonical( $context );
2005 
2006  if ( $this->getTitle()->isConversionTable() ) {
2007  // @todo ConversionTable should become a separate content model, so
2008  // we don't need special cases like this one.
2009  $options->disableContentConversion();
2010  }
2011 
2012  return $options;
2013  }
2014 
2034  public function prepareContentForEdit(
2035  Content $content,
2036  $revision = null,
2037  User $user = null,
2038  $serialFormat = null,
2039  $useCache = true
2040  ) {
2041  global $wgUser;
2042 
2043  if ( !$user ) {
2044  $user = $wgUser;
2045  }
2046 
2047  if ( $revision !== null ) {
2048  if ( $revision instanceof Revision ) {
2049  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
2050  $revision = $revision->getRevisionRecord();
2051  } elseif ( !( $revision instanceof RevisionRecord ) ) {
2052  throw new InvalidArgumentException(
2053  __METHOD__ . ': invalid $revision argument type ' . gettype( $revision ) );
2054  }
2055  }
2056 
2057  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2058  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2059 
2060  if ( !$updater->isUpdatePrepared() ) {
2061  $updater->prepareContent( $user, $slots, $useCache );
2062 
2063  if ( $revision ) {
2064  $updater->prepareUpdate(
2065  $revision,
2066  [
2067  'causeAction' => 'prepare-edit',
2068  'causeAgent' => $user->getName(),
2069  ]
2070  );
2071  }
2072  }
2073 
2074  return $updater->getPreparedEdit();
2075  }
2076 
2106  public function doEditUpdates( $revisionRecord, User $user, array $options = [] ) {
2107  if ( $revisionRecord instanceof Revision ) {
2108  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
2109  $revisionRecord = $revisionRecord->getRevisionRecord();
2110  }
2111  if ( isset( $options['oldrevision'] ) && $options['oldrevision'] instanceof Revision ) {
2112  wfDeprecated(
2113  __METHOD__ . ' with the `oldrevision` option being a ' .
2114  'Revision object',
2115  '1.35'
2116  );
2117  $options['oldrevision'] = $options['oldrevision']->getRevisionRecord();
2118  }
2119 
2120  $options += [
2121  'causeAction' => 'edit-page',
2122  'causeAgent' => $user->getName(),
2123  ];
2124 
2125  $updater = $this->getDerivedDataUpdater( $user, $revisionRecord );
2126 
2127  $updater->prepareUpdate( $revisionRecord, $options );
2128 
2129  $updater->doUpdates();
2130  }
2131 
2145  public function updateParserCache( array $options = [] ) {
2146  $revision = $this->getRevisionRecord();
2147  if ( !$revision || !$revision->getId() ) {
2148  LoggerFactory::getInstance( 'wikipage' )->info(
2149  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2150  );
2151  return;
2152  }
2153  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2154 
2155  $updater = $this->getDerivedDataUpdater( $user, $revision );
2156  $updater->prepareUpdate( $revision, $options );
2157  $updater->doParserCacheUpdate();
2158  }
2159 
2189  public function doSecondaryDataUpdates( array $options = [] ) {
2190  $options['recursive'] = $options['recursive'] ?? true;
2191  $revision = $this->getRevisionRecord();
2192  if ( !$revision || !$revision->getId() ) {
2193  LoggerFactory::getInstance( 'wikipage' )->info(
2194  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2195  );
2196  return;
2197  }
2198  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2199 
2200  $updater = $this->getDerivedDataUpdater( $user, $revision );
2201  $updater->prepareUpdate( $revision, $options );
2202  $updater->doSecondaryDataUpdates( $options );
2203  }
2204 
2219  public function doUpdateRestrictions( array $limit, array $expiry,
2220  &$cascade, $reason, User $user, $tags = null
2221  ) {
2223 
2224  if ( wfReadOnly() ) {
2225  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2226  }
2227 
2228  $this->loadPageData( 'fromdbmaster' );
2229  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2230  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2231  $id = $this->getId();
2232 
2233  if ( !$cascade ) {
2234  $cascade = false;
2235  }
2236 
2237  // Take this opportunity to purge out expired restrictions
2239 
2240  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2241  // we expect a single selection, but the schema allows otherwise.
2242  $isProtected = false;
2243  $protect = false;
2244  $changed = false;
2245 
2246  $dbw = wfGetDB( DB_MASTER );
2247 
2248  foreach ( $restrictionTypes as $action ) {
2249  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2250  $expiry[$action] = 'infinity';
2251  }
2252  if ( !isset( $limit[$action] ) ) {
2253  $limit[$action] = '';
2254  } elseif ( $limit[$action] != '' ) {
2255  $protect = true;
2256  }
2257 
2258  // Get current restrictions on $action
2259  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2260  if ( $current != '' ) {
2261  $isProtected = true;
2262  }
2263 
2264  if ( $limit[$action] != $current ) {
2265  $changed = true;
2266  } elseif ( $limit[$action] != '' ) {
2267  // Only check expiry change if the action is actually being
2268  // protected, since expiry does nothing on an not-protected
2269  // action.
2270  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2271  $changed = true;
2272  }
2273  }
2274  }
2275 
2276  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2277  $changed = true;
2278  }
2279 
2280  // If nothing has changed, do nothing
2281  if ( !$changed ) {
2282  return Status::newGood();
2283  }
2284 
2285  if ( !$protect ) { // No protection at all means unprotection
2286  $revCommentMsg = 'unprotectedarticle-comment';
2287  $logAction = 'unprotect';
2288  } elseif ( $isProtected ) {
2289  $revCommentMsg = 'modifiedarticleprotection-comment';
2290  $logAction = 'modify';
2291  } else {
2292  $revCommentMsg = 'protectedarticle-comment';
2293  $logAction = 'protect';
2294  }
2295 
2296  $logRelationsValues = [];
2297  $logRelationsField = null;
2298  $logParamsDetails = [];
2299 
2300  // Null revision (used for change tag insertion)
2301  $nullRevision = null;
2302 
2303  if ( $id ) { // Protection of existing page
2304  if ( !$this->getHookRunner()->onArticleProtect( $this, $user, $limit, $reason ) ) {
2305  return Status::newGood();
2306  }
2307 
2308  // Only certain restrictions can cascade...
2309  $editrestriction = isset( $limit['edit'] )
2310  ? [ $limit['edit'] ]
2311  : $this->mTitle->getRestrictions( 'edit' );
2312  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2313  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2314  }
2315  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2316  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2317  }
2318 
2319  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2320  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2321  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2322  }
2323  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2324  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2325  }
2326 
2327  // The schema allows multiple restrictions
2328  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2329  $cascade = false;
2330  }
2331 
2332  // insert null revision to identify the page protection change as edit summary
2333  $latest = $this->getLatest();
2334  $nullRevisionRecord = $this->insertNullProtectionRevision(
2335  $revCommentMsg,
2336  $limit,
2337  $expiry,
2338  $cascade,
2339  $reason,
2340  $user
2341  );
2342 
2343  if ( $nullRevisionRecord === null ) {
2344  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2345  }
2346 
2347  $logRelationsField = 'pr_id';
2348 
2349  // T214035: Avoid deadlock on MySQL.
2350  // Do a DELETE by primary key (pr_id) for any existing protection rows.
2351  // On MySQL and derivatives, unconditionally deleting by page ID (pr_page) would.
2352  // place a gap lock if there are no matching rows. This can deadlock when another
2353  // thread modifies protection settings for page IDs in the same gap.
2354  $existingProtectionIds = $dbw->selectFieldValues(
2355  'page_restrictions',
2356  'pr_id',
2357  [
2358  'pr_page' => $id,
2359  'pr_type' => array_map( 'strval', array_keys( $limit ) )
2360  ],
2361  __METHOD__
2362  );
2363 
2364  if ( $existingProtectionIds ) {
2365  $dbw->delete(
2366  'page_restrictions',
2367  [ 'pr_id' => $existingProtectionIds ],
2368  __METHOD__
2369  );
2370  }
2371 
2372  // Update restrictions table
2373  foreach ( $limit as $action => $restrictions ) {
2374  if ( $restrictions != '' ) {
2375  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2376  $dbw->insert(
2377  'page_restrictions',
2378  [
2379  'pr_page' => $id,
2380  'pr_type' => $action,
2381  'pr_level' => $restrictions,
2382  'pr_cascade' => $cascadeValue,
2383  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2384  ],
2385  __METHOD__
2386  );
2387  $logRelationsValues[] = $dbw->insertId();
2388  $logParamsDetails[] = [
2389  'type' => $action,
2390  'level' => $restrictions,
2391  'expiry' => $expiry[$action],
2392  'cascade' => (bool)$cascadeValue,
2393  ];
2394  }
2395  }
2396 
2397  // Clear out legacy restriction fields
2398  $dbw->update(
2399  'page',
2400  [ 'page_restrictions' => '' ],
2401  [ 'page_id' => $id ],
2402  __METHOD__
2403  );
2404 
2405  $this->getHookRunner()->onRevisionFromEditComplete(
2406  $this, $nullRevisionRecord, $latest, $user, $tags );
2407 
2408  // Hook is hard deprecated since 1.35
2409  if ( $this->getHookContainer()->isRegistered( 'NewRevisionFromEditComplete' ) ) {
2410  // Only create the Revision object if neeed
2411  $nullRevision = new Revision( $nullRevisionRecord );
2412  $this->getHookRunner()->onNewRevisionFromEditComplete(
2413  $this, $nullRevision, $latest, $user, $tags );
2414  }
2415 
2416  $this->getHookRunner()->onArticleProtectComplete( $this, $user, $limit, $reason );
2417  } else { // Protection of non-existing page (also known as "title protection")
2418  // Cascade protection is meaningless in this case
2419  $cascade = false;
2420 
2421  if ( $limit['create'] != '' ) {
2422  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2423  $dbw->replace( 'protected_titles',
2424  [ [ 'pt_namespace', 'pt_title' ] ],
2425  [
2426  'pt_namespace' => $this->mTitle->getNamespace(),
2427  'pt_title' => $this->mTitle->getDBkey(),
2428  'pt_create_perm' => $limit['create'],
2429  'pt_timestamp' => $dbw->timestamp(),
2430  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2431  'pt_user' => $user->getId(),
2432  ] + $commentFields, __METHOD__
2433  );
2434  $logParamsDetails[] = [
2435  'type' => 'create',
2436  'level' => $limit['create'],
2437  'expiry' => $expiry['create'],
2438  ];
2439  } else {
2440  $dbw->delete( 'protected_titles',
2441  [
2442  'pt_namespace' => $this->mTitle->getNamespace(),
2443  'pt_title' => $this->mTitle->getDBkey()
2444  ], __METHOD__
2445  );
2446  }
2447  }
2448 
2449  $this->mTitle->flushRestrictions();
2450  InfoAction::invalidateCache( $this->mTitle );
2451 
2452  if ( $logAction == 'unprotect' ) {
2453  $params = [];
2454  } else {
2455  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2456  $params = [
2457  '4::description' => $protectDescriptionLog, // parameter for IRC
2458  '5:bool:cascade' => $cascade,
2459  'details' => $logParamsDetails, // parameter for localize and api
2460  ];
2461  }
2462 
2463  // Update the protection log
2464  $logEntry = new ManualLogEntry( 'protect', $logAction );
2465  $logEntry->setTarget( $this->mTitle );
2466  $logEntry->setComment( $reason );
2467  $logEntry->setPerformer( $user );
2468  $logEntry->setParameters( $params );
2469  if ( $nullRevision !== null ) {
2470  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2471  }
2472  $logEntry->addTags( $tags );
2473  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2474  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2475  }
2476  $logId = $logEntry->insert();
2477  $logEntry->publish( $logId );
2478 
2479  return Status::newGood( $logId );
2480  }
2481 
2496  string $revCommentMsg,
2497  array $limit,
2498  array $expiry,
2499  bool $cascade,
2500  string $reason,
2501  User $user
2502  ) : ?RevisionRecord {
2503  $dbw = wfGetDB( DB_MASTER );
2504 
2505  // Prepare a null revision to be added to the history
2506  $editComment = wfMessage(
2507  $revCommentMsg,
2508  $this->mTitle->getPrefixedText(),
2509  $user->getName()
2510  )->inContentLanguage()->text();
2511  if ( $reason ) {
2512  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2513  }
2514  $protectDescription = $this->protectDescription( $limit, $expiry );
2515  if ( $protectDescription ) {
2516  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2517  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2518  ->inContentLanguage()->text();
2519  }
2520  if ( $cascade ) {
2521  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2522  $editComment .= wfMessage( 'brackets' )->params(
2523  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2524  )->inContentLanguage()->text();
2525  }
2526 
2527  $revStore = $this->getRevisionStore();
2528  $comment = CommentStoreComment::newUnsavedComment( $editComment );
2529  $nullRevRecord = $revStore->newNullRevision(
2530  $dbw,
2531  $this->getTitle(),
2532  $comment,
2533  true,
2534  $user
2535  );
2536 
2537  if ( $nullRevRecord ) {
2538  $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
2539 
2540  // Update page record and touch page
2541  $oldLatest = $inserted->getParentId();
2542 
2543  $this->updateRevisionOn( $dbw, $inserted, $oldLatest );
2544 
2545  return $inserted;
2546  } else {
2547  return null;
2548  }
2549  }
2550 
2555  protected function formatExpiry( $expiry ) {
2556  if ( $expiry != 'infinity' ) {
2557  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2558  return wfMessage(
2559  'protect-expiring',
2560  $contLang->timeanddate( $expiry, false, false ),
2561  $contLang->date( $expiry, false, false ),
2562  $contLang->time( $expiry, false, false )
2563  )->inContentLanguage()->text();
2564  } else {
2565  return wfMessage( 'protect-expiry-indefinite' )
2566  ->inContentLanguage()->text();
2567  }
2568  }
2569 
2577  public function protectDescription( array $limit, array $expiry ) {
2578  $protectDescription = '';
2579 
2580  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2581  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2582  # All possible message keys are listed here for easier grepping:
2583  # * restriction-create
2584  # * restriction-edit
2585  # * restriction-move
2586  # * restriction-upload
2587  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2588  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2589  # with '' filtered out. All possible message keys are listed below:
2590  # * protect-level-autoconfirmed
2591  # * protect-level-sysop
2592  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2593  ->inContentLanguage()->text();
2594 
2595  $expiryText = $this->formatExpiry( $expiry[$action] );
2596 
2597  if ( $protectDescription !== '' ) {
2598  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2599  }
2600  $protectDescription .= wfMessage( 'protect-summary-desc' )
2601  ->params( $actionText, $restrictionsText, $expiryText )
2602  ->inContentLanguage()->text();
2603  }
2604 
2605  return $protectDescription;
2606  }
2607 
2619  public function protectDescriptionLog( array $limit, array $expiry ) {
2620  $protectDescriptionLog = '';
2621 
2622  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2623  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2624  $expiryText = $this->formatExpiry( $expiry[$action] );
2625  $protectDescriptionLog .=
2626  $dirMark .
2627  "[$action=$restrictions] ($expiryText)";
2628  }
2629 
2630  return trim( $protectDescriptionLog );
2631  }
2632 
2645  public function isBatchedDelete( $safetyMargin = 0 ) {
2647 
2648  $dbr = wfGetDB( DB_REPLICA );
2649  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2650  $revCount += $safetyMargin;
2651 
2652  return $revCount >= $wgDeleteRevisionsBatchSize;
2653  }
2654 
2680  public function doDeleteArticleReal(
2681  $reason, User $deleter, $suppress = false, $u1 = null, &$error = '', $u2 = null,
2682  $tags = [], $logsubtype = 'delete', $immediate = false
2683  ) {
2684  wfDebug( __METHOD__ );
2685 
2686  $status = Status::newGood();
2687 
2688  if ( !$this->getHookRunner()->onArticleDelete(
2689  $this, $deleter, $reason, $error, $status, $suppress )
2690  ) {
2691  if ( $status->isOK() ) {
2692  // Hook aborted but didn't set a fatal status
2693  $status->fatal( 'delete-hook-aborted' );
2694  }
2695  return $status;
2696  }
2697 
2698  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2699  $logsubtype, $immediate );
2700  }
2701 
2718  public function doDeleteArticleBatched(
2719  $reason, $suppress, User $deleter, $tags,
2720  $logsubtype, $immediate = false, $webRequestId = null
2721  ) {
2722  wfDebug( __METHOD__ );
2723 
2724  $status = Status::newGood();
2725 
2726  $dbw = wfGetDB( DB_MASTER );
2727  $dbw->startAtomic( __METHOD__ );
2728 
2729  $this->loadPageData( self::READ_LATEST );
2730  $id = $this->getId();
2731  // T98706: lock the page from various other updates but avoid using
2732  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2733  // the revisions queries (which also JOIN on user). Only lock the page
2734  // row and CAS check on page_latest to see if the trx snapshot matches.
2735  $lockedLatest = $this->lockAndGetLatest();
2736  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2737  $dbw->endAtomic( __METHOD__ );
2738  // Page not there or trx snapshot is stale
2739  $status->error( 'cannotdelete',
2740  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2741  return $status;
2742  }
2743 
2744  // At this point we are now committed to returning an OK
2745  // status unless some DB query error or other exception comes up.
2746  // This way callers don't have to call rollback() if $status is bad
2747  // unless they actually try to catch exceptions (which is rare).
2748 
2749  // we need to remember the old content so we can use it to generate all deletion updates.
2750  $revisionRecord = $this->getRevisionRecord();
2751  try {
2752  $content = $this->getContent( RevisionRecord::RAW );
2753  } catch ( Exception $ex ) {
2754  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2755  . $ex->getMessage() );
2756 
2757  $content = null;
2758  }
2759 
2760  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2761  // one batch of revisions and defer archival of any others to the job queue.
2762  $explictTrxLogged = false;
2763  while ( true ) {
2764  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2765  if ( $done || !$immediate ) {
2766  break;
2767  }
2768  $dbw->endAtomic( __METHOD__ );
2769  if ( $dbw->explicitTrxActive() ) {
2770  // Explict transactions may never happen here in practice. Log to be sure.
2771  if ( !$explictTrxLogged ) {
2772  $explictTrxLogged = true;
2773  LoggerFactory::getInstance( 'wfDebug' )->debug(
2774  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2775  'title' => $this->getTitle()->getText(),
2776  ] );
2777  }
2778  continue;
2779  }
2780  if ( $dbw->trxLevel() ) {
2781  $dbw->commit( __METHOD__ );
2782  }
2783  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2784  $lbFactory->waitForReplication();
2785  $dbw->startAtomic( __METHOD__ );
2786  }
2787 
2788  // If done archiving, also delete the article.
2789  if ( !$done ) {
2790  $dbw->endAtomic( __METHOD__ );
2791 
2792  $jobParams = [
2793  'namespace' => $this->getTitle()->getNamespace(),
2794  'title' => $this->getTitle()->getDBkey(),
2795  'wikiPageId' => $id,
2796  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2797  'reason' => $reason,
2798  'suppress' => $suppress,
2799  'userId' => $deleter->getId(),
2800  'tags' => json_encode( $tags ),
2801  'logsubtype' => $logsubtype,
2802  ];
2803 
2804  $job = new DeletePageJob( $jobParams );
2805  JobQueueGroup::singleton()->push( $job );
2806 
2807  $status->warning( 'delete-scheduled',
2808  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2809  } else {
2810  // Get archivedRevisionCount by db query, because there's no better alternative.
2811  // Jobs cannot pass a count of archived revisions to the next job, because additional
2812  // deletion operations can be started while the first is running. Jobs from each
2813  // gracefully interleave, but would not know about each other's count. Deduplication
2814  // in the job queue to avoid simultaneous deletion operations would add overhead.
2815  // Number of archived revisions cannot be known beforehand, because edits can be made
2816  // while deletion operations are being processed, changing the number of archivals.
2817  $archivedRevisionCount = (int)$dbw->selectField(
2818  'archive', 'COUNT(*)',
2819  [
2820  'ar_namespace' => $this->getTitle()->getNamespace(),
2821  'ar_title' => $this->getTitle()->getDBkey(),
2822  'ar_page_id' => $id
2823  ], __METHOD__
2824  );
2825 
2826  // Clone the title and wikiPage, so we have the information we need when
2827  // we log and run the ArticleDeleteComplete hook.
2828  $logTitle = clone $this->mTitle;
2829  $wikiPageBeforeDelete = clone $this;
2830 
2831  // Now that it's safely backed up, delete it
2832  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2833 
2834  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2835  $logtype = $suppress ? 'suppress' : 'delete';
2836 
2837  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2838  $logEntry->setPerformer( $deleter );
2839  $logEntry->setTarget( $logTitle );
2840  $logEntry->setComment( $reason );
2841  $logEntry->addTags( $tags );
2842  $logid = $logEntry->insert();
2843 
2844  $dbw->onTransactionPreCommitOrIdle(
2845  function () use ( $logEntry, $logid ) {
2846  // T58776: avoid deadlocks (especially from FileDeleteForm)
2847  $logEntry->publish( $logid );
2848  },
2849  __METHOD__
2850  );
2851 
2852  $dbw->endAtomic( __METHOD__ );
2853 
2854  $this->doDeleteUpdates(
2855  $id,
2856  $content,
2857  $revisionRecord,
2858  $deleter
2859  );
2860 
2861  $this->getHookRunner()->onArticleDeleteComplete(
2862  $wikiPageBeforeDelete,
2863  $deleter,
2864  $reason,
2865  $id,
2866  $content,
2867  $logEntry,
2868  $archivedRevisionCount
2869  );
2870  $status->value = $logid;
2871 
2872  // Show log excerpt on 404 pages rather than just a link
2873  $dbCache = ObjectCache::getInstance( 'db-replicated' );
2874  $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2875  $dbCache->set( $key, 1, $dbCache::TTL_DAY );
2876  }
2877 
2878  return $status;
2879  }
2880 
2890  protected function archiveRevisions( $dbw, $id, $suppress ) {
2892 
2893  // Given the lock above, we can be confident in the title and page ID values
2894  $namespace = $this->getTitle()->getNamespace();
2895  $dbKey = $this->getTitle()->getDBkey();
2896 
2897  $commentStore = CommentStore::getStore();
2898  $actorMigration = ActorMigration::newMigration();
2899 
2900  $revQuery = $this->getRevisionStore()->getQueryInfo();
2901  $bitfield = false;
2902 
2903  // Bitfields to further suppress the content
2904  if ( $suppress ) {
2905  $bitfield = RevisionRecord::SUPPRESSED_ALL;
2906  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2907  }
2908 
2909  // For now, shunt the revision data into the archive table.
2910  // Text is *not* removed from the text table; bulk storage
2911  // is left intact to avoid breaking block-compression or
2912  // immutable storage schemes.
2913  // In the future, we may keep revisions and mark them with
2914  // the rev_deleted field, which is reserved for this purpose.
2915 
2916  // Lock rows in `revision` and its temp tables, but not any others.
2917  // Note array_intersect() preserves keys from the first arg, and we're
2918  // assuming $revQuery has `revision` primary and isn't using subtables
2919  // for anything we care about.
2920  $dbw->lockForUpdate(
2921  array_intersect(
2922  $revQuery['tables'],
2923  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2924  ),
2925  [ 'rev_page' => $id ],
2926  __METHOD__,
2927  [],
2928  $revQuery['joins']
2929  );
2930 
2931  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2932  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2933  $res = $dbw->select(
2934  $revQuery['tables'],
2935  $revQuery['fields'],
2936  [ 'rev_page' => $id ],
2937  __METHOD__,
2938  [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2939  $revQuery['joins']
2940  );
2941 
2942  // Build their equivalent archive rows
2943  $rowsInsert = [];
2944  $revids = [];
2945 
2947  $ipRevIds = [];
2948 
2949  $done = true;
2950  foreach ( $res as $row ) {
2951  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2952  $done = false;
2953  break;
2954  }
2955 
2956  $comment = $commentStore->getComment( 'rev_comment', $row );
2957  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2958  $rowInsert = [
2959  'ar_namespace' => $namespace,
2960  'ar_title' => $dbKey,
2961  'ar_timestamp' => $row->rev_timestamp,
2962  'ar_minor_edit' => $row->rev_minor_edit,
2963  'ar_rev_id' => $row->rev_id,
2964  'ar_parent_id' => $row->rev_parent_id,
2965  'ar_len' => $row->rev_len,
2966  'ar_page_id' => $id,
2967  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2968  'ar_sha1' => $row->rev_sha1,
2969  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2970  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2971 
2972  $rowsInsert[] = $rowInsert;
2973  $revids[] = $row->rev_id;
2974 
2975  // Keep track of IP edits, so that the corresponding rows can
2976  // be deleted in the ip_changes table.
2977  if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
2978  $ipRevIds[] = $row->rev_id;
2979  }
2980  }
2981 
2982  // This conditional is just a sanity check
2983  if ( count( $revids ) > 0 ) {
2984  // Copy them into the archive table
2985  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2986 
2987  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2988  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2989  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2990 
2991  // Also delete records from ip_changes as applicable.
2992  if ( count( $ipRevIds ) > 0 ) {
2993  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2994  }
2995  }
2996 
2997  return $done;
2998  }
2999 
3006  public function lockAndGetLatest() {
3007  return (int)wfGetDB( DB_MASTER )->selectField(
3008  'page',
3009  'page_latest',
3010  [
3011  'page_id' => $this->getId(),
3012  // Typically page_id is enough, but some code might try to do
3013  // updates assuming the title is the same, so verify that
3014  'page_namespace' => $this->getTitle()->getNamespace(),
3015  'page_title' => $this->getTitle()->getDBkey()
3016  ],
3017  __METHOD__,
3018  [ 'FOR UPDATE' ]
3019  );
3020  }
3021 
3035  public function doDeleteUpdates(
3036  $id, Content $content = null, $revRecord = null, User $user = null
3037  ) {
3038  if ( $revRecord && $revRecord instanceof Revision ) {
3039  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
3040  $revRecord = $revRecord->getRevisionRecord();
3041  }
3042 
3043  if ( $id !== $this->getId() ) {
3044  throw new InvalidArgumentException( 'Mismatching page ID' );
3045  }
3046 
3047  try {
3048  $countable = $this->isCountable();
3049  } catch ( Exception $ex ) {
3050  // fallback for deleting broken pages for which we cannot load the content for
3051  // some reason. Note that doDeleteArticleReal() already logged this problem.
3052  $countable = false;
3053  }
3054 
3055  // Update site status
3057  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3058  ) );
3059 
3060  // Delete pagelinks, update secondary indexes, etc
3061  $updates = $this->getDeletionUpdates( $revRecord ?: $content );
3062  foreach ( $updates as $update ) {
3063  DeferredUpdates::addUpdate( $update );
3064  }
3065 
3066  $causeAgent = $user ? $user->getName() : 'unknown';
3067  // Reparse any pages transcluding this page
3069  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3070  // Reparse any pages including this image
3071  if ( $this->mTitle->getNamespace() === NS_FILE ) {
3073  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3074  }
3075 
3076  // Clear caches
3077  self::onArticleDelete( $this->mTitle );
3078 
3080  $this->mTitle,
3081  $revRecord,
3082  null,
3084  );
3085 
3086  // Reset this object and the Title object
3087  $this->loadFromRow( false, self::READ_LATEST );
3088 
3089  // Search engine
3090  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3091  }
3092 
3124  public function doRollback(
3125  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3126  ) {
3127  $resultDetails = null;
3128 
3129  // Check permissions
3130  $permManager = MediaWikiServices::getInstance()->getPermissionManager();
3131  $editErrors = $permManager->getPermissionErrors( 'edit', $user, $this->mTitle );
3132  $rollbackErrors = $permManager->getPermissionErrors( 'rollback', $user, $this->mTitle );
3133  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3134 
3135  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3136  $errors[] = [ 'sessionfailure' ];
3137  }
3138 
3139  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3140  $errors[] = [ 'actionthrottledtext' ];
3141  }
3142 
3143  // If there were errors, bail out now
3144  if ( !empty( $errors ) ) {
3145  return $errors;
3146  }
3147 
3148  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3149  }
3150 
3173  public function commitRollback( $fromP, $summary, $bot,
3174  &$resultDetails, User $guser, $tags = null
3175  ) {
3177 
3178  $dbw = wfGetDB( DB_MASTER );
3179 
3180  if ( wfReadOnly() ) {
3181  return [ [ 'readonlytext' ] ];
3182  }
3183 
3184  // Begin revision creation cycle by creating a PageUpdater.
3185  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3186  $updater = $this->newPageUpdater( $guser );
3187  $current = $updater->grabParentRevision();
3188 
3189  if ( $current === null ) {
3190  // Something wrong... no page?
3191  return [ [ 'notanarticle' ] ];
3192  }
3193 
3194  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3195  $legacyCurrentCallback = function () use ( $current ) {
3196  // Only created when needed
3197  return new Revision( $current );
3198  };
3199  $from = str_replace( '_', ' ', $fromP );
3200 
3201  // User name given should match up with the top revision.
3202  // If the revision's user is not visible, then $from should be empty.
3203  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3204  $resultDetails = new DeprecatablePropertyArray(
3205  [
3206  'current' => $legacyCurrentCallback,
3207  'current-revision-record' => $current,
3208  ],
3209  [ 'current' => '1.35' ],
3210  __METHOD__
3211  );
3212  return [ [ 'alreadyrolled',
3213  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3214  htmlspecialchars( $fromP ),
3215  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3216  ] ];
3217  }
3218 
3219  // Get the last edit not by this person...
3220  // Note: these may not be public values
3221  $actorWhere = ActorMigration::newMigration()->getWhere(
3222  $dbw,
3223  'rev_user',
3224  $current->getUser( RevisionRecord::RAW )
3225  );
3226 
3227  $s = $dbw->selectRow(
3228  [ 'revision' ] + $actorWhere['tables'],
3229  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3230  [
3231  'rev_page' => $current->getPageId(),
3232  'NOT(' . $actorWhere['conds'] . ')',
3233  ],
3234  __METHOD__,
3235  [
3236  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3237  'ORDER BY' => [ 'rev_timestamp DESC', 'rev_id DESC' ]
3238  ],
3239  $actorWhere['joins']
3240  );
3241  if ( $s === false ) {
3242  // No one else ever edited this page
3243  return [ [ 'cantrollback' ] ];
3244  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3245  || $s->rev_deleted & RevisionRecord::DELETED_USER
3246  ) {
3247  // Only admins can see this text
3248  return [ [ 'notvisiblerev' ] ];
3249  }
3250 
3251  // Generate the edit summary if necessary
3252  $target = $this->getRevisionStore()->getRevisionById(
3253  $s->rev_id,
3254  RevisionStore::READ_LATEST
3255  );
3256  if ( empty( $summary ) ) {
3257  if ( !$currentEditorForPublic ) { // no public user name
3258  $summary = wfMessage( 'revertpage-nouser' );
3259  } elseif ( $wgDisableAnonTalk && $current->getUser() === 0 ) {
3260  $summary = wfMessage( 'revertpage-anon' );
3261  } else {
3262  $summary = wfMessage( 'revertpage' );
3263  }
3264  }
3265  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3266 
3267  // Allow the custom summary to use the same args as the default message
3268  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3269  $args = [
3270  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3271  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3272  $s->rev_id,
3273  $contLang->timeanddate( MWTimestamp::convert( TS_MW, $s->rev_timestamp ) ),
3274  $current->getId(),
3275  $contLang->timeanddate( $current->getTimestamp() )
3276  ];
3277  if ( $summary instanceof Message ) {
3278  $summary = $summary->params( $args )->inContentLanguage()->text();
3279  } else {
3280  $summary = wfMsgReplaceArgs( $summary, $args );
3281  }
3282 
3283  // Trim spaces on user supplied text
3284  $summary = trim( $summary );
3285 
3286  // Save
3287  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3288 
3289  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
3290  if ( $permissionManager->userHasRight( $guser, 'minoredit' ) ) {
3291  $flags |= EDIT_MINOR;
3292  }
3293 
3294  if ( $bot && ( $permissionManager->userHasAnyRight( $guser, 'markbotedits', 'bot' ) ) ) {
3295  $flags |= EDIT_FORCE_BOT;
3296  }
3297 
3298  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3299  $currentContent = $current->getContent( SlotRecord::MAIN );
3300  $targetContent = $target->getContent( SlotRecord::MAIN );
3301  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3302 
3303  // Build rollback revision:
3304  // Restore old content
3305  // TODO: MCR: test this once we can store multiple slots
3306  foreach ( $target->getSlots()->getSlots() as $slot ) {
3307  $updater->inheritSlot( $slot );
3308  }
3309 
3310  // Remove extra slots
3311  // TODO: MCR: test this once we can store multiple slots
3312  foreach ( $current->getSlotRoles() as $role ) {
3313  if ( !$target->hasSlot( $role ) ) {
3314  $updater->removeSlot( $role );
3315  }
3316  }
3317 
3318  $updater->setOriginalRevisionId( $target->getId() );
3319  $oldestRevertedRevision = $this->getRevisionStore()->getNextRevision(
3320  $target,
3321  RevisionStore::READ_LATEST
3322  );
3323  if ( $oldestRevertedRevision !== null ) {
3324  $updater->markAsRevert(
3325  EditResult::REVERT_ROLLBACK,
3326  $oldestRevertedRevision->getId(),
3327  $current->getId()
3328  );
3329  }
3330 
3331  // TODO: this logic should not be in the storage layer, it's here for compatibility
3332  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3333  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3334 
3335  if ( $wgUseRCPatrol && $permissionManager->userCan(
3336  'autopatrol', $guser, $this->getTitle()
3337  ) ) {
3338  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3339  }
3340 
3341  // Actually store the rollback
3342  $rev = $updater->saveRevision(
3344  $flags
3345  );
3346 
3347  // Set patrolling and bot flag on the edits, which gets rollbacked.
3348  // This is done even on edit failure to have patrolling in that case (T64157).
3349  $set = [];
3350  if ( $bot && $permissionManager->userHasRight( $guser, 'markbotedits' ) ) {
3351  // Mark all reverted edits as bot
3352  $set['rc_bot'] = 1;
3353  }
3354 
3355  if ( $wgUseRCPatrol ) {
3356  // Mark all reverted edits as patrolled
3357  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3358  }
3359 
3360  if ( count( $set ) ) {
3361  $actorWhere = ActorMigration::newMigration()->getWhere(
3362  $dbw,
3363  'rc_user',
3364  $current->getUser( RevisionRecord::RAW ),
3365  false
3366  );
3367  $dbw->update( 'recentchanges', $set,
3368  [ /* WHERE */
3369  'rc_cur_id' => $current->getPageId(),
3370  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3371  $actorWhere['conds'], // No tables/joins are needed for rc_user
3372  ],
3373  __METHOD__
3374  );
3375  }
3376 
3377  if ( !$updater->wasSuccessful() ) {
3378  return $updater->getStatus()->getErrorsArray();
3379  }
3380 
3381  // Report if the edit was not created because it did not change the content.
3382  if ( $updater->isUnchanged() ) {
3383  $resultDetails = new DeprecatablePropertyArray(
3384  [
3385  'current' => $legacyCurrentCallback,
3386  'current-revision-record' => $current,
3387  ],
3388  [ 'current' => '1.35' ],
3389  __METHOD__
3390  );
3391  return [ [ 'alreadyrolled',
3392  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3393  htmlspecialchars( $fromP ),
3394  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3395  ] ];
3396  }
3397 
3398  if ( $changingContentModel ) {
3399  // If the content model changed during the rollback,
3400  // make sure it gets logged to Special:Log/contentmodel
3401  $log = new ManualLogEntry( 'contentmodel', 'change' );
3402  $log->setPerformer( $guser );
3403  $log->setTarget( $this->mTitle );
3404  $log->setComment( $summary );
3405  $log->setParameters( [
3406  '4::oldmodel' => $currentContent->getModel(),
3407  '5::newmodel' => $targetContent->getModel(),
3408  ] );
3409 
3410  $logId = $log->insert( $dbw );
3411  $log->publish( $logId );
3412  }
3413 
3414  $revId = $rev->getId();
3415 
3416  // Hook is hard deprecated since 1.35
3417  if ( $this->getHookContainer()->isRegistered( 'ArticleRollbackComplete' ) ) {
3418  // Only create the Revision objects if needed
3419  $legacyCurrent = new Revision( $current );
3420  $legacyTarget = new Revision( $target );
3421  $this->getHookRunner()->onArticleRollbackComplete( $this, $guser,
3422  $legacyTarget, $legacyCurrent );
3423  }
3424 
3425  $this->getHookRunner()->onRollbackComplete( $this, $guser, $target, $current );
3426 
3427  $legacyTargetCallback = function () use ( $target ) {
3428  // Only create the Revision object if needed
3429  return new Revision( $target );
3430  };
3431 
3432  $tags = array_merge(
3433  $tags ?: [],
3434  $updater->getEditResult()->getRevertTags()
3435  );
3436 
3437  $resultDetails = new DeprecatablePropertyArray(
3438  [
3439  'summary' => $summary,
3440  'current' => $legacyCurrentCallback,
3441  'current-revision-record' => $current,
3442  'target' => $legacyTargetCallback,
3443  'target-revision-record' => $target,
3444  'newid' => $revId,
3445  'tags' => $tags
3446  ],
3447  [ 'current' => '1.35', 'target' => '1.35' ],
3448  __METHOD__
3449  );
3450 
3451  // TODO: make this return a Status object and wrap $resultDetails in that.
3452  return [];
3453  }
3454 
3466  public static function onArticleCreate( Title $title ) {
3467  // TODO: move this into a PageEventEmitter service
3468 
3469  // Update existence markers on article/talk tabs...
3470  $other = $title->getOtherPage();
3471 
3472  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3473  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3474 
3475  $title->touchLinks();
3476  $title->deleteTitleProtection();
3477 
3478  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3479 
3480  // Invalidate caches of articles which include this page
3482  $title,
3483  'templatelinks',
3484  [ 'causeAction' => 'page-create' ]
3485  );
3486  JobQueueGroup::singleton()->lazyPush( $job );
3487 
3488  if ( $title->getNamespace() === NS_CATEGORY ) {
3489  // Load the Category object, which will schedule a job to create
3490  // the category table row if necessary. Checking a replica DB is ok
3491  // here, in the worst case it'll run an unnecessary recount job on
3492  // a category that probably doesn't have many members.
3493  Category::newFromTitle( $title )->getID();
3494  }
3495  }
3496 
3502  public static function onArticleDelete( Title $title ) {
3503  // TODO: move this into a PageEventEmitter service
3504 
3505  // Update existence markers on article/talk tabs...
3506  $other = $title->getOtherPage();
3507 
3508  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3509  $hcu->purgeTitleUrls( [ $title, $other ], $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3510 
3511  $title->touchLinks();
3512 
3513  $services = MediaWikiServices::getInstance();
3514  $services->getLinkCache()->invalidateTitle( $title );
3515 
3517 
3518  // Messages
3519  if ( $title->getNamespace() === NS_MEDIAWIKI ) {
3520  $services->getMessageCache()->updateMessageOverride( $title, null );
3521  }
3522 
3523  // Images
3524  if ( $title->getNamespace() === NS_FILE ) {
3526  $title,
3527  'imagelinks',
3528  [ 'causeAction' => 'page-delete' ]
3529  );
3530  JobQueueGroup::singleton()->lazyPush( $job );
3531  }
3532 
3533  // User talk pages
3534  if ( $title->getNamespace() === NS_USER_TALK ) {
3535  $user = User::newFromName( $title->getText(), false );
3536  if ( $user ) {
3537  MediaWikiServices::getInstance()
3538  ->getTalkPageNotificationManager()
3539  ->removeUserHasNewMessages( $user );
3540  }
3541  }
3542 
3543  // Image redirects
3544  $services->getRepoGroup()->getLocalRepo()->invalidateImageRedirect( $title );
3545 
3546  // Purge cross-wiki cache entities referencing this page
3548  }
3549 
3559  public static function onArticleEdit(
3560  Title $title,
3561  $revRecord = null,
3562  $slotsChanged = null
3563  ) {
3564  if ( $revRecord && $revRecord instanceof Revision ) {
3565  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
3566  $revRecord = $revRecord->getRevisionRecord();
3567  }
3568 
3569  // TODO: move this into a PageEventEmitter service
3570 
3571  $jobs = [];
3572  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3573  // Invalidate caches of articles which include this page.
3574  // Only for the main slot, because only the main slot is transcluded.
3575  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3577  $title,
3578  'templatelinks',
3579  [ 'causeAction' => 'page-edit' ]
3580  );
3581  }
3582  // Invalidate the caches of all pages which redirect here
3584  $title,
3585  'redirect',
3586  [ 'causeAction' => 'page-edit' ]
3587  );
3588  JobQueueGroup::singleton()->lazyPush( $jobs );
3589 
3590  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3591 
3592  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
3593  $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
3594 
3595  // Purge ?action=info cache
3596  $revid = $revRecord ? $revRecord->getId() : null;
3597  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3599  } );
3600 
3601  // Purge cross-wiki cache entities referencing this page
3603  }
3604 
3612  private static function purgeInterwikiCheckKey( Title $title ) {
3614 
3615  if ( !$wgEnableScaryTranscluding ) {
3616  return; // @todo: perhaps this wiki is only used as a *source* for content?
3617  }
3618 
3619  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3620  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3621  $cache->resetCheckKey(
3622  // Do not include the namespace since there can be multiple aliases to it
3623  // due to different namespace text definitions on different wikis. This only
3624  // means that some cache invalidations happen that are not strictly needed.
3625  $cache->makeGlobalKey(
3626  'interwiki-page',
3628  $title->getDBkey()
3629  )
3630  );
3631  } );
3632  }
3633 
3640  public function getCategories() {
3641  $id = $this->getId();
3642  if ( $id == 0 ) {
3643  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3644  }
3645 
3646  $dbr = wfGetDB( DB_REPLICA );
3647  $res = $dbr->select( 'categorylinks',
3648  [ 'page_title' => 'cl_to', 'page_namespace' => NS_CATEGORY ],
3649  [ 'cl_from' => $id ],
3650  __METHOD__
3651  );
3652 
3653  return TitleArray::newFromResult( $res );
3654  }
3655 
3662  public function getHiddenCategories() {
3663  $result = [];
3664  $id = $this->getId();
3665 
3666  if ( $id == 0 ) {
3667  return [];
3668  }
3669 
3670  $dbr = wfGetDB( DB_REPLICA );
3671  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3672  [ 'cl_to' ],
3673  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3674  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3675  __METHOD__ );
3676 
3677  if ( $res !== false ) {
3678  foreach ( $res as $row ) {
3679  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3680  }
3681  }
3682 
3683  return $result;
3684  }
3685 
3693  public function getAutoDeleteReason( &$hasHistory ) {
3694  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3695  }
3696 
3707  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3708  $id = $id ?: $this->getId();
3709  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3710  getCategoryLinkType( $this->getTitle()->getNamespace() );
3711 
3712  $addFields = [ 'cat_pages = cat_pages + 1' ];
3713  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3714  if ( $type !== 'page' ) {
3715  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3716  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3717  }
3718 
3719  $dbw = wfGetDB( DB_MASTER );
3720 
3721  if ( count( $added ) ) {
3722  $existingAdded = $dbw->selectFieldValues(
3723  'category',
3724  'cat_title',
3725  [ 'cat_title' => $added ],
3726  __METHOD__
3727  );
3728 
3729  // For category rows that already exist, do a plain
3730  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3731  // to avoid creating gaps in the cat_id sequence.
3732  if ( count( $existingAdded ) ) {
3733  $dbw->update(
3734  'category',
3735  $addFields,
3736  [ 'cat_title' => $existingAdded ],
3737  __METHOD__
3738  );
3739  }
3740 
3741  $missingAdded = array_diff( $added, $existingAdded );
3742  if ( count( $missingAdded ) ) {
3743  $insertRows = [];
3744  foreach ( $missingAdded as $cat ) {
3745  $insertRows[] = [
3746  'cat_title' => $cat,
3747  'cat_pages' => 1,
3748  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3749  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3750  ];
3751  }
3752  $dbw->upsert(
3753  'category',
3754  $insertRows,
3755  'cat_title',
3756  $addFields,
3757  __METHOD__
3758  );
3759  }
3760  }
3761 
3762  if ( count( $deleted ) ) {
3763  $dbw->update(
3764  'category',
3765  $removeFields,
3766  [ 'cat_title' => $deleted ],
3767  __METHOD__
3768  );
3769  }
3770 
3771  foreach ( $added as $catName ) {
3772  $cat = Category::newFromName( $catName );
3773  $this->getHookRunner()->onCategoryAfterPageAdded( $cat, $this );
3774  }
3775 
3776  foreach ( $deleted as $catName ) {
3777  $cat = Category::newFromName( $catName );
3778  $this->getHookRunner()->onCategoryAfterPageRemoved( $cat, $this, $id );
3779  // Refresh counts on categories that should be empty now (after commit, T166757)
3780  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3781  $cat->refreshCountsIfEmpty();
3782  } );
3783  }
3784  }
3785 
3792  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3793  if ( wfReadOnly() ) {
3794  return;
3795  }
3796 
3797  if ( !$this->getHookRunner()->onOpportunisticLinksUpdate( $this,
3798  $this->mTitle, $parserOutput )
3799  ) {
3800  return;
3801  }
3802 
3803  $config = RequestContext::getMain()->getConfig();
3804 
3805  $params = [
3806  'isOpportunistic' => true,
3807  'rootJobTimestamp' => $parserOutput->getCacheTime()
3808  ];
3809 
3810  if ( $this->mTitle->areRestrictionsCascading() ) {
3811  // If the page is cascade protecting, the links should really be up-to-date
3812  JobQueueGroup::singleton()->lazyPush(
3813  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3814  );
3815  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3816  // Assume the output contains "dynamic" time/random based magic words.
3817  // Only update pages that expired due to dynamic content and NOT due to edits
3818  // to referenced templates/files. When the cache expires due to dynamic content,
3819  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3820  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3821  // template/file edit already triggered recursive RefreshLinksJob jobs.
3822  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3823  // If a page is uncacheable, do not keep spamming a job for it.
3824  // Although it would be de-duplicated, it would still waste I/O.
3826  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3827  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3828  if ( $cache->add( $key, time(), $ttl ) ) {
3829  JobQueueGroup::singleton()->lazyPush(
3830  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3831  );
3832  }
3833  }
3834  }
3835  }
3836 
3846  public function getDeletionUpdates( $rev = null ) {
3847  if ( !$rev ) {
3848  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3849 
3850  try {
3851  $rev = $this->getRevisionRecord();
3852  } catch ( Exception $ex ) {
3853  // If we can't load the content, something is wrong. Perhaps that's why
3854  // the user is trying to delete the page, so let's not fail in that case.
3855  // Note that doDeleteArticleReal() will already have logged an issue with
3856  // loading the content.
3857  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3858  }
3859  }
3860 
3861  if ( !$rev ) {
3862  $slotContent = [];
3863  } elseif ( $rev instanceof Content ) {
3864  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3865 
3866  $slotContent = [ SlotRecord::MAIN => $rev ];
3867  } else {
3868  $slotContent = array_map( function ( SlotRecord $slot ) {
3869  return $slot->getContent();
3870  }, $rev->getSlots()->getSlots() );
3871  }
3872 
3873  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3874 
3875  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3876  // model here, not the content object!
3877  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3879  foreach ( $slotContent as $role => $content ) {
3880  $handler = $content->getContentHandler();
3881 
3882  $updates = $handler->getDeletionUpdates(
3883  $this->getTitle(),
3884  $role
3885  );
3886  $allUpdates = array_merge( $allUpdates, $updates );
3887 
3888  // TODO: remove B/C hack in 1.32!
3889  $legacyUpdates = $content->getDeletionUpdates( $this );
3890 
3891  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3892  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3893  return !( $update instanceof LinksDeletionUpdate );
3894  } );
3895 
3896  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3897  }
3898 
3899  $this->getHookRunner()->onPageDeletionDataUpdates(
3900  $this->getTitle(), $rev, $allUpdates );
3901 
3902  // TODO: hard deprecate old hook in 1.33
3903  $this->getHookRunner()->onWikiPageDeletionUpdates( $this, $content, $allUpdates );
3904  return $allUpdates;
3905  }
3906 
3914  public function isLocal() {
3915  return true;
3916  }
3917 
3927  public function getWikiDisplayName() {
3928  global $wgSitename;
3929  return $wgSitename;
3930  }
3931 
3940  public function getSourceURL() {
3941  return $this->getTitle()->getCanonicalURL();
3942  }
3943 
3950  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3951 
3952  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3953  }
3954 
3961  public function __wakeup() {
3962  // Make sure we re-fetch the latest state from the database.
3963  // In particular, the latest revision may have changed.
3964  // As a side-effect, this makes sure mLastRevision doesn't
3965  // end up being an instance of the old Revision class (see T259181).
3966  $this->clear();
3967  }
3968 
3969 }
WikiPage\hasDifferencesOutsideMainSlot
static hasDifferencesOutsideMainSlot( $a, $b)
Helper method for checking whether two revisions have differences that go beyond the main slot.
Definition: WikiPage.php:1502
WikiPage\getCategories
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:3640
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:44
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
Page
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:29
WikiPage\getComment
getComment( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:869
CacheTime\getCacheExpiry
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:182
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:579
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:3466
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:431
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
WikiPage\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3693
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:748
ParserOutput
Definition: ParserOutput.php:27
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:295
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
WikiPage\getRedirectTarget
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:954
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:272
User\getId
getId()
Get the user's ID.
Definition: User.php:2033
WikiPage\clearCacheFields
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:303
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:2645
WikiPage\updateRevisionOn
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1334
WikiPage\wasLoadedFrom
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:476
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:1531
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:160
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:829
EDIT_INTERNAL
const EDIT_INTERNAL
Definition: Defines.php:148
MediaWiki\Storage\DerivedPageDataUpdater\setArticleCountMethod
setArticleCountMethod( $articleCountMethod)
Definition: DerivedPageDataUpdater.php:451
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(Title $title, ?RevisionRecord $old, ?RevisionRecord $new, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:542
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:81
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:1796
WikiPage\hasViewableContent
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition: WikiPage.php:569
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:640
WikiPage\doViewUpdates
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1231
MediaWiki\Storage\DerivedPageDataUpdater\setRcWatchCategoryMembership
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
Definition: DerivedPageDataUpdater.php:459
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1617
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1663
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:434
SearchUpdate
Database independant search index updater.
Definition: SearchUpdate.php:33
WikiPage\$mDataLoadedFrom
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:103
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred update queue for execution at the appropriate time.
Definition: DeferredUpdates.php:106
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:54
WikiPage\replaceSectionContent
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1579
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:2003
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1078
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1126
wfMsgReplaceArgs
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
Definition: GlobalFunctions.php:1255
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:542
WikiPage\getRevision
getRevision()
Get the latest revision.
Definition: WikiPage.php:734
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:1220
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:876
$s
$s
Definition: mergeMessageFileList.php:184
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:1116
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:611
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1087
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:1661
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
Definition: FakeResultWrapper.php:11
WikiPage\archiveRevisions
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:2890
WikiPage\getSlotRoleRegistry
getSlotRoleRegistry()
Definition: WikiPage.php:231
WikiPage\getDBLoadBalancer
getDBLoadBalancer()
Definition: WikiPage.php:252
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:262
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:7332
$revQuery
$revQuery
Definition: testCompression.php:56
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:7348
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:140
HTMLCacheUpdateJob\newForBacklinks
static newForBacklinks(Title $title, $table, $params=[])
Definition: HTMLCacheUpdateJob.php:59
WikiPage\$mTitle
Title $mTitle
Definition: WikiPage.php:65
WikiPage\$mTouched
string $mTouched
Definition: WikiPage.php:123
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:848
WikiPage\triggerOpportunisticLinksUpdate
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3792
WikiPage\protectDescription
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2577
WikiPage\$mLastRevision
RevisionRecord $mLastRevision
Definition: WikiPage.php:113
$dbr
$dbr
Definition: testCompression.php:54
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:3785
WikiPage\updateParserCache
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2145
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1561
$wgEnableScaryTranscluding
$wgEnableScaryTranscluding
Enable interwiki transcluding.
Definition: DefaultSettings.php:4721
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:1025
MWException
MediaWiki exception.
Definition: MWException.php:29
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:159
WikiPage\doDeleteArticleBatched
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2718
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:884
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:25
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1027
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:3124
MediaWiki\Storage\DerivedPageDataUpdater\setLogger
setLogger(LoggerInterface $logger)
Definition: DerivedPageDataUpdater.php:338
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:112
WikiPage\doSecondaryDataUpdates
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2189
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2475
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:323
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:1189
WikiPage\getId
getId()
Definition: WikiPage.php:544
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1289
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1167
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:284
MediaWiki\Content\ContentHandlerFactory
Definition: ContentHandlerFactory.php:44
WikiPage\exists
exists()
Definition: WikiPage.php:554
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:146
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3502
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:108
WikiPage\__construct
__construct(Title $title)
Definition: WikiPage.php:138
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:629
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:85
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:651
WikiPage\purgeInterwikiCheckKey
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3612
WikiPage\$mDataLoaded
bool $mDataLoaded
Definition: WikiPage.php:72
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:78
$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:3035
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:591
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:794
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:662
ParserOptions\getStubThreshold
getStubThreshold()
Thumb size preferred by the user.
Definition: ParserOptions.php:547
WikiPage\insertNullProtectionRevision
insertNullProtectionRevision(string $revCommentMsg, array $limit, array $expiry, bool $cascade, string $reason, User $user)
Insert a new null revision for this page.
Definition: WikiPage.php:2495
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:630
DB_MASTER
const DB_MASTER
Definition: defines.php:26
$revStore
$revStore
Definition: testCompression.php:55
IDBAccessObject\READ_NONE
const READ_NONE
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:75
WikiPage\$mLatest
int false $mLatest
False means "not loaded".
Definition: WikiPage.php:86
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:910
WikiPage\doPurge
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1254
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:901
User\clearNotification
clearNotification(&$title, $oldid=0)
Clear the user's notification timestamp for the given title.
Definition: User.php:3210
WikiPage\getContentModel
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:596
MediaWiki\Storage\EditResult
Object for storing information about the effects of an edit.
Definition: EditResult.php:38
WikiPage\pageDataFromTitle
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object's namespace and title using a sanitized title string.
Definition: WikiPage.php:401
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:3006
Wikimedia\Rdbms\LoadBalancer
Database connection, tracking, load balancing, and transaction manager for a cluster.
Definition: LoadBalancer.php:42
$wgPageLanguageUseDB
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
Definition: DefaultSettings.php:9171
Category\newFromTitle
static newFromTitle( $title)
Factory function.
Definition: Category.php:153
WikiPage\getDerivedDataUpdater
getDerivedDataUpdater(User $forUser=null, RevisionRecord $forRevision=null, RevisionSlotsUpdate $forUpdate=null, $forEdit=false)
Returns a DerivedPageDataUpdater for use with the given target revision or new content.
Definition: WikiPage.php:1740
WikiPage\updateIfNewerOn
updateIfNewerOn( $dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record.
Definition: WikiPage.php:1452
MediaWiki\Storage\RevisionSlotsUpdate
Value object representing a modification of revision slots.
Definition: RevisionSlotsUpdate.php:36
Revision\RevisionRenderer
The RevisionRenderer service provides access to rendered output for revisions.
Definition: RevisionRenderer.php:45
$content
$content
Definition: router.php:76
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:72
WikiPage\getDeletionUpdates
getDeletionUpdates( $rev=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3846
WikiPage\protectDescriptionLog
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2619
WikiPage\insertRedirect
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1001
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:142
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:57
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
MediaWiki\Storage\EditResultCache
Class allowing easy storage and retrieval of EditResults associated with revisions.
Definition: EditResultCache.php:42
WikiPage\doEditUpdates
doEditUpdates( $revisionRecord, User $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2106
MediaWiki\Storage\PageUpdater
Controller-like object for creating and updating pages by creating new revisions.
Definition: PageUpdater.php:77
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:125
WikiPage\newFromID
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:174
$wgSitename
$wgSitename
Name of the site.
Definition: DefaultSettings.php:80
WikiPage\getSourceURL
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3940
ParserOptions\newCanonical
static newCanonical( $context=null, $userLang=null)
Creates a "canonical" ParserOptions object.
Definition: ParserOptions.php:1124
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1494
WikiPage\$derivedDataUpdater
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:133
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:3173
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3662
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:190
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:84
$wgDeleteRevisionsBatchSize
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
Definition: DefaultSettings.php:5953
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:454
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:1890
WikiPage\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object.
Definition: WikiPage.php:336
WikiPage\getOldestRevision
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:675
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:685
WikiPage\setLastEdit
setLastEdit(RevisionRecord $revRecord)
Set the latest revision.
Definition: WikiPage.php:724
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1067
Content
Base interface for content objects.
Definition: Content.php:35
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:141
WikiPage\loadFromRow
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:502
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:5747
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2555
Title
Represents a title within MediaWiki.
Definition: Title.php:41
WikiPage\getUser
getUser( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:808
wfRandom
wfRandom()
Get a random decimal value in the domain of [0, 1), in a way not likely to give duplicate values for ...
Definition: GlobalFunctions.php:255
WikiPage\$mIsRedirect
bool $mIsRedirect
Definition: WikiPage.php:79
WikiPage\newDerivedDataUpdater
newDerivedDataUpdater()
Definition: WikiPage.php:1678
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:1139
$cache
$cache
Definition: mcc.php:33
DeferredUpdates\PRESEND
const PRESEND
Definition: DeferredUpdates.php:84
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:50
WebRequest\getRequestId
static getRequestId()
Get the unique request ID.
Definition: WebRequest.php:327
WikiPage\$mId
int $mId
Definition: WikiPage.php:98
DeletePageJob
Class DeletePageJob.
Definition: DeletePageJob.php:6
WikiPage\getWikiDisplayName
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3927
LinksUpdate\queueRecursiveJobsForTable
static queueRecursiveJobsForTable(Title $title, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
Definition: LinksUpdate.php:369
WikiPage\prepareContentForEdit
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:2034
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:161
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:200
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:3707
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3949
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:42
WikiPage\pageData
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:372
Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
MediaWiki\Edit\PreparedEdit
Represents information returned by WikiPage::prepareContentForEdit()
Definition: PreparedEdit.php:35
WikiPage\isLocal
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3914
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:1074
$wgArticleCountMethod
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article.
Definition: DefaultSettings.php:4764
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:77
Category\newFromName
static newFromName( $name)
Factory function.
Definition: Category.php:133
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:143
ParserOptions\isSafeToCache
isSafeToCache()
Test whether these options are safe to cache.
Definition: ParserOptions.php:1451
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:1027
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:780
WikiPage\getRevisionRenderer
getRevisionRenderer()
Definition: WikiPage.php:224
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1415
WikiPage\$mPreparedEdit
PreparedEdit false $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:93
Revision\getRevisionRecord
getRevisionRecord()
Definition: Revision.php:448
WikiPage\$mLinksUpdated
string $mLinksUpdated
Definition: WikiPage.php:128
WikiPage\onArticleEdit
static onArticleEdit(Title $title, $revRecord=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3559
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:578
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:104
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:109
CommentStore\getStore
static getStore()
Definition: CommentStore.php:120
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:145
ParserOutput\hasDynamicContent
hasDynamicContent()
Check whether the cache TTL was lowered due to dynamic content.
Definition: ParserOutput.php:1342
WikiPage\getParserCache
getParserCache()
Definition: WikiPage.php:245
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:415
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:275
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:769
WikiPage\__wakeup
__wakeup()
Ensure consistency when unserializing.
Definition: WikiPage.php:3961
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2062
CommentStoreComment
Value object for a comment stored by CommentStore.
Definition: CommentStoreComment.php:30
WikiPage\$mTimestamp
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition: WikiPage.php:118
MediaWiki\Debug\DeprecatablePropertyArray
ArrayAccess implementation that supports deprecating access to certain properties.
Definition: DeprecatablePropertyArray.php:16
Title\purgeExpiredRestrictions
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:2966
WikiPage\doDeleteArticleReal
doDeleteArticleReal( $reason, User $deleter, $suppress=false, $u1=null, &$error='', $u2=null, $tags=[], $logsubtype='delete', $immediate=false)
Back-end article deletion Deletes the article with database consistency, writes logs,...
Definition: WikiPage.php:2680
$wgDisableAnonTalk
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
Definition: DefaultSettings.php:7445
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
$wgRCWatchCategoryMembership
$wgRCWatchCategoryMembership
Treat category membership changes as a RecentChange.
Definition: DefaultSettings.php:7322
WikiPage\getContentHandlerFactory
getContentHandlerFactory()
Definition: WikiPage.php:238
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:292
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:2219
WikiPage\getRevisionStore
getRevisionStore()
Definition: WikiPage.php:217
$type
$type
Definition: testCompression.php:52