MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
38 
45 class WikiPage implements Page, IDBAccessObject {
46  // Constants for $mDataLoadedFrom and related
47 
51  public $mTitle = null;
52 
56  public $mDataLoaded = false; // !< Boolean
57  public $mIsRedirect = false; // !< Boolean
58  public $mLatest = false; // !< Integer (false means "not loaded")
62  public $mPreparedEdit = false;
63 
67  protected $mId = null;
68 
72  protected $mDataLoadedFrom = self::READ_NONE;
73 
77  protected $mRedirectTarget = null;
78 
82  protected $mLastRevision = null;
83 
87  protected $mTimestamp = '';
88 
92  protected $mTouched = '19700101000000';
93 
97  protected $mLinksUpdated = '19700101000000';
98 
103 
108  public function __construct( Title $title ) {
109  $this->mTitle = $title;
110  }
111 
116  public function __clone() {
117  $this->mTitle = clone $this->mTitle;
118  }
119 
128  public static function factory( Title $title ) {
129  $ns = $title->getNamespace();
130 
131  if ( $ns == NS_MEDIA ) {
132  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
133  } elseif ( $ns < 0 ) {
134  throw new MWException( "Invalid or virtual namespace $ns given." );
135  }
136 
137  $page = null;
138  if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
139  return $page;
140  }
141 
142  switch ( $ns ) {
143  case NS_FILE:
144  $page = new WikiFilePage( $title );
145  break;
146  case NS_CATEGORY:
147  $page = new WikiCategoryPage( $title );
148  break;
149  default:
150  $page = new WikiPage( $title );
151  }
152 
153  return $page;
154  }
155 
166  public static function newFromID( $id, $from = 'fromdb' ) {
167  // page ids are never 0 or negative, see T63166
168  if ( $id < 1 ) {
169  return null;
170  }
171 
172  $from = self::convertSelectType( $from );
173  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
174  $pageQuery = self::getQueryInfo();
175  $row = $db->selectRow(
176  $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
177  [], $pageQuery['joins']
178  );
179  if ( !$row ) {
180  return null;
181  }
182  return self::newFromRow( $row, $from );
183  }
184 
196  public static function newFromRow( $row, $from = 'fromdb' ) {
197  $page = self::factory( Title::newFromRow( $row ) );
198  $page->loadFromRow( $row, $from );
199  return $page;
200  }
201 
208  protected static function convertSelectType( $type ) {
209  switch ( $type ) {
210  case 'fromdb':
211  return self::READ_NORMAL;
212  case 'fromdbmaster':
213  return self::READ_LATEST;
214  case 'forupdate':
215  return self::READ_LOCKING;
216  default:
217  // It may already be an integer or whatever else
218  return $type;
219  }
220  }
221 
225  private function getRevisionStore() {
226  return MediaWikiServices::getInstance()->getRevisionStore();
227  }
228 
232  private function getRevisionRenderer() {
233  return MediaWikiServices::getInstance()->getRevisionRenderer();
234  }
235 
239  private function getSlotRoleRegistry() {
240  return MediaWikiServices::getInstance()->getSlotRoleRegistry();
241  }
242 
246  private function getParserCache() {
247  return MediaWikiServices::getInstance()->getParserCache();
248  }
249 
253  private function getDBLoadBalancer() {
254  return MediaWikiServices::getInstance()->getDBLoadBalancer();
255  }
256 
263  public function getActionOverrides() {
264  return $this->getContentHandler()->getActionOverrides();
265  }
266 
276  public function getContentHandler() {
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 
334  public static function selectFields() {
336 
337  wfDeprecated( __METHOD__, '1.31' );
338 
339  $fields = [
340  'page_id',
341  'page_namespace',
342  'page_title',
343  'page_restrictions',
344  'page_is_redirect',
345  'page_is_new',
346  'page_random',
347  'page_touched',
348  'page_links_updated',
349  'page_latest',
350  'page_len',
351  ];
352 
353  if ( $wgContentHandlerUseDB ) {
354  $fields[] = 'page_content_model';
355  }
356 
357  if ( $wgPageLanguageUseDB ) {
358  $fields[] = 'page_lang';
359  }
360 
361  return $fields;
362  }
363 
373  public static function getQueryInfo() {
375 
376  $ret = [
377  'tables' => [ 'page' ],
378  'fields' => [
379  'page_id',
380  'page_namespace',
381  'page_title',
382  'page_restrictions',
383  'page_is_redirect',
384  'page_is_new',
385  'page_random',
386  'page_touched',
387  'page_links_updated',
388  'page_latest',
389  'page_len',
390  ],
391  'joins' => [],
392  ];
393 
394  if ( $wgContentHandlerUseDB ) {
395  $ret['fields'][] = 'page_content_model';
396  }
397 
398  if ( $wgPageLanguageUseDB ) {
399  $ret['fields'][] = 'page_lang';
400  }
401 
402  return $ret;
403  }
404 
412  protected function pageData( $dbr, $conditions, $options = [] ) {
413  $pageQuery = self::getQueryInfo();
414 
415  // Avoid PHP 7.1 warning of passing $this by reference
416  $wikiPage = $this;
417 
418  Hooks::run( 'ArticlePageDataBefore', [
419  &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
420  ] );
421 
422  $row = $dbr->selectRow(
423  $pageQuery['tables'],
424  $pageQuery['fields'],
425  $conditions,
426  __METHOD__,
427  $options,
428  $pageQuery['joins']
429  );
430 
431  Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
432 
433  return $row;
434  }
435 
445  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
446  return $this->pageData( $dbr, [
447  'page_namespace' => $title->getNamespace(),
448  'page_title' => $title->getDBkey() ], $options );
449  }
450 
459  public function pageDataFromId( $dbr, $id, $options = [] ) {
460  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
461  }
462 
475  public function loadPageData( $from = 'fromdb' ) {
476  $from = self::convertSelectType( $from );
477  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
478  // We already have the data from the correct location, no need to load it twice.
479  return;
480  }
481 
482  if ( is_int( $from ) ) {
483  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
484  $loadBalancer = $this->getDBLoadBalancer();
485  $db = $loadBalancer->getConnection( $index );
486  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
487 
488  if ( !$data
489  && $index == DB_REPLICA
490  && $loadBalancer->getServerCount() > 1
491  && $loadBalancer->hasOrMadeRecentMasterChanges()
492  ) {
493  $from = self::READ_LATEST;
494  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
495  $db = $loadBalancer->getConnection( $index );
496  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
497  }
498  } else {
499  // No idea from where the caller got this data, assume replica DB.
500  $data = $from;
501  $from = self::READ_NORMAL;
502  }
503 
504  $this->loadFromRow( $data, $from );
505  }
506 
520  public function wasLoadedFrom( $from ) {
521  $from = self::convertSelectType( $from );
522 
523  if ( !is_int( $from ) ) {
524  // No idea from where the caller got this data, assume replica DB.
525  $from = self::READ_NORMAL;
526  }
527 
528  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
529  return true;
530  }
531 
532  return false;
533  }
534 
546  public function loadFromRow( $data, $from ) {
547  $lc = MediaWikiServices::getInstance()->getLinkCache();
548  $lc->clearLink( $this->mTitle );
549 
550  if ( $data ) {
551  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
552 
553  $this->mTitle->loadFromRow( $data );
554 
555  // Old-fashioned restrictions
556  $this->mTitle->loadRestrictions( $data->page_restrictions );
557 
558  $this->mId = intval( $data->page_id );
559  $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
560  $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
561  $this->mIsRedirect = intval( $data->page_is_redirect );
562  $this->mLatest = intval( $data->page_latest );
563  // T39225: $latest may no longer match the cached latest Revision object.
564  // Double-check the ID of any cached latest Revision object for consistency.
565  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
566  $this->mLastRevision = null;
567  $this->mTimestamp = '';
568  }
569  } else {
570  $lc->addBadLinkObj( $this->mTitle );
571 
572  $this->mTitle->loadFromRow( false );
573 
574  $this->clearCacheFields();
575 
576  $this->mId = 0;
577  }
578 
579  $this->mDataLoaded = true;
580  $this->mDataLoadedFrom = self::convertSelectType( $from );
581  }
582 
586  public function getId() {
587  if ( !$this->mDataLoaded ) {
588  $this->loadPageData();
589  }
590  return $this->mId;
591  }
592 
596  public function exists() {
597  if ( !$this->mDataLoaded ) {
598  $this->loadPageData();
599  }
600  return $this->mId > 0;
601  }
602 
611  public function hasViewableContent() {
612  return $this->mTitle->isKnown();
613  }
614 
620  public function isRedirect() {
621  if ( !$this->mDataLoaded ) {
622  $this->loadPageData();
623  }
624 
625  return (bool)$this->mIsRedirect;
626  }
627 
638  public function getContentModel() {
639  if ( $this->exists() ) {
641 
642  return $cache->getWithSetCallback(
643  $cache->makeKey( 'page-content-model', $this->getLatest() ),
644  $cache::TTL_MONTH,
645  function () {
646  $rev = $this->getRevision();
647  if ( $rev ) {
648  // Look at the revision's actual content model
649  return $rev->getContentModel();
650  } else {
651  $title = $this->mTitle->getPrefixedDBkey();
652  wfWarn( "Page $title exists but has no (visible) revisions!" );
653  return $this->mTitle->getContentModel();
654  }
655  }
656  );
657  }
658 
659  // use the default model for this page
660  return $this->mTitle->getContentModel();
661  }
662 
667  public function checkTouched() {
668  if ( !$this->mDataLoaded ) {
669  $this->loadPageData();
670  }
671  return ( $this->mId && !$this->mIsRedirect );
672  }
673 
678  public function getTouched() {
679  if ( !$this->mDataLoaded ) {
680  $this->loadPageData();
681  }
682  return $this->mTouched;
683  }
684 
689  public function getLinksTimestamp() {
690  if ( !$this->mDataLoaded ) {
691  $this->loadPageData();
692  }
693  return $this->mLinksUpdated;
694  }
695 
700  public function getLatest() {
701  if ( !$this->mDataLoaded ) {
702  $this->loadPageData();
703  }
704  return (int)$this->mLatest;
705  }
706 
711  public function getOldestRevision() {
712  // Try using the replica DB first, then try the master
713  $rev = $this->mTitle->getFirstRevision();
714  if ( !$rev ) {
715  $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE );
716  }
717  return $rev;
718  }
719 
724  protected function loadLastEdit() {
725  if ( $this->mLastRevision !== null ) {
726  return; // already loaded
727  }
728 
729  $latest = $this->getLatest();
730  if ( !$latest ) {
731  return; // page doesn't exist or is missing page_latest info
732  }
733 
734  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
735  // T39225: if session S1 loads the page row FOR UPDATE, the result always
736  // includes the latest changes committed. This is true even within REPEATABLE-READ
737  // transactions, where S1 normally only sees changes committed before the first S1
738  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
739  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
740  // happened after the first S1 SELECT.
741  // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
742  $flags = Revision::READ_LOCKING;
743  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
744  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
745  // Bug T93976: if page_latest was loaded from the master, fetch the
746  // revision from there as well, as it may not exist yet on a replica DB.
747  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
748  $flags = Revision::READ_LATEST;
749  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
750  } else {
751  $dbr = wfGetDB( DB_REPLICA );
752  $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
753  }
754 
755  if ( $revision ) { // sanity
756  $this->setLastEdit( $revision );
757  }
758  }
759 
764  protected function setLastEdit( Revision $revision ) {
765  $this->mLastRevision = $revision;
766  $this->mTimestamp = $revision->getTimestamp();
767  }
768 
773  public function getRevision() {
774  $this->loadLastEdit();
775  if ( $this->mLastRevision ) {
776  return $this->mLastRevision;
777  }
778  return null;
779  }
780 
785  public function getRevisionRecord() {
786  $this->loadLastEdit();
787  if ( $this->mLastRevision ) {
788  return $this->mLastRevision->getRevisionRecord();
789  }
790  return null;
791  }
792 
806  public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
807  $this->loadLastEdit();
808  if ( $this->mLastRevision ) {
809  return $this->mLastRevision->getContent( $audience, $user );
810  }
811  return null;
812  }
813 
817  public function getTimestamp() {
818  // Check if the field has been filled by WikiPage::setTimestamp()
819  if ( !$this->mTimestamp ) {
820  $this->loadLastEdit();
821  }
822 
823  return wfTimestamp( TS_MW, $this->mTimestamp );
824  }
825 
831  public function setTimestamp( $ts ) {
832  $this->mTimestamp = wfTimestamp( TS_MW, $ts );
833  }
834 
844  public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
845  $this->loadLastEdit();
846  if ( $this->mLastRevision ) {
847  return $this->mLastRevision->getUser( $audience, $user );
848  } else {
849  return -1;
850  }
851  }
852 
863  public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
864  $revision = $this->getOldestRevision();
865  if ( $revision ) {
866  $userName = $revision->getUserText( $audience, $user );
867  return User::newFromName( $userName, false );
868  } else {
869  return null;
870  }
871  }
872 
882  public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
883  $this->loadLastEdit();
884  if ( $this->mLastRevision ) {
885  return $this->mLastRevision->getUserText( $audience, $user );
886  } else {
887  return '';
888  }
889  }
890 
900  public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
901  $this->loadLastEdit();
902  if ( $this->mLastRevision ) {
903  return $this->mLastRevision->getComment( $audience, $user );
904  } else {
905  return '';
906  }
907  }
908 
914  public function getMinorEdit() {
915  $this->loadLastEdit();
916  if ( $this->mLastRevision ) {
917  return $this->mLastRevision->isMinor();
918  } else {
919  return false;
920  }
921  }
922 
931  public function isCountable( $editInfo = false ) {
932  global $wgArticleCountMethod;
933 
934  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
935 
936  if ( !$this->mTitle->isContentPage() ) {
937  return false;
938  }
939 
940  if ( $editInfo ) {
941  // NOTE: only the main slot can make a page a redirect
942  $content = $editInfo->pstContent;
943  } else {
944  $content = $this->getContent();
945  }
946 
947  if ( !$content || $content->isRedirect() ) {
948  return false;
949  }
950 
951  $hasLinks = null;
952 
953  if ( $wgArticleCountMethod === 'link' ) {
954  // nasty special case to avoid re-parsing to detect links
955 
956  if ( $editInfo ) {
957  // ParserOutput::getLinks() is a 2D array of page links, so
958  // to be really correct we would need to recurse in the array
959  // but the main array should only have items in it if there are
960  // links.
961  $hasLinks = (bool)count( $editInfo->output->getLinks() );
962  } else {
963  // NOTE: keep in sync with RevisionRenderer::getLinkCount
964  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
965  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
966  [ 'pl_from' => $this->getId() ], __METHOD__ );
967  }
968  }
969 
970  // TODO: MCR: determine $hasLinks for each slot, and use that info
971  // with that slot's Content's isCountable method. That requires per-
972  // slot ParserOutput in the ParserCache, or per-slot info in the
973  // pagelinks table.
974  return $content->isCountable( $hasLinks );
975  }
976 
984  public function getRedirectTarget() {
985  if ( !$this->mTitle->isRedirect() ) {
986  return null;
987  }
988 
989  if ( $this->mRedirectTarget !== null ) {
990  return $this->mRedirectTarget;
991  }
992 
993  // Query the redirect table
994  $dbr = wfGetDB( DB_REPLICA );
995  $row = $dbr->selectRow( 'redirect',
996  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
997  [ 'rd_from' => $this->getId() ],
998  __METHOD__
999  );
1000 
1001  // rd_fragment and rd_interwiki were added later, populate them if empty
1002  if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
1003  // (T203942) We can't redirect to Media namespace because it's virtual.
1004  // We don't want to modify Title objects farther down the
1005  // line. So, let's fix this here by changing to File namespace.
1006  if ( $row->rd_namespace == NS_MEDIA ) {
1007  $namespace = NS_FILE;
1008  } else {
1009  $namespace = $row->rd_namespace;
1010  }
1011  $this->mRedirectTarget = Title::makeTitle(
1012  $namespace, $row->rd_title,
1013  $row->rd_fragment, $row->rd_interwiki
1014  );
1015  return $this->mRedirectTarget;
1016  }
1017 
1018  // This page doesn't have an entry in the redirect table
1019  $this->mRedirectTarget = $this->insertRedirect();
1020  return $this->mRedirectTarget;
1021  }
1022 
1031  public function insertRedirect() {
1032  $content = $this->getContent();
1033  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1034  if ( !$retval ) {
1035  return null;
1036  }
1037 
1038  // Update the DB post-send if the page has not cached since now
1039  $latest = $this->getLatest();
1041  function () use ( $retval, $latest ) {
1042  $this->insertRedirectEntry( $retval, $latest );
1043  },
1045  wfGetDB( DB_MASTER )
1046  );
1047 
1048  return $retval;
1049  }
1050 
1056  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1057  $dbw = wfGetDB( DB_MASTER );
1058  $dbw->startAtomic( __METHOD__ );
1059 
1060  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1061  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1062  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1063  $dbw->upsert(
1064  'redirect',
1065  [
1066  'rd_from' => $this->getId(),
1067  'rd_namespace' => $rt->getNamespace(),
1068  'rd_title' => $rt->getDBkey(),
1069  'rd_fragment' => $truncatedFragment,
1070  'rd_interwiki' => $rt->getInterwiki(),
1071  ],
1072  [ 'rd_from' ],
1073  [
1074  'rd_namespace' => $rt->getNamespace(),
1075  'rd_title' => $rt->getDBkey(),
1076  'rd_fragment' => $truncatedFragment,
1077  'rd_interwiki' => $rt->getInterwiki(),
1078  ],
1079  __METHOD__
1080  );
1081  }
1082 
1083  $dbw->endAtomic( __METHOD__ );
1084  }
1085 
1091  public function followRedirect() {
1092  return $this->getRedirectURL( $this->getRedirectTarget() );
1093  }
1094 
1102  public function getRedirectURL( $rt ) {
1103  if ( !$rt ) {
1104  return false;
1105  }
1106 
1107  if ( $rt->isExternal() ) {
1108  if ( $rt->isLocal() ) {
1109  // Offsite wikis need an HTTP redirect.
1110  // This can be hard to reverse and may produce loops,
1111  // so they may be disabled in the site configuration.
1112  $source = $this->mTitle->getFullURL( 'redirect=no' );
1113  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1114  } else {
1115  // External pages without "local" bit set are not valid
1116  // redirect targets
1117  return false;
1118  }
1119  }
1120 
1121  if ( $rt->isSpecialPage() ) {
1122  // Gotta handle redirects to special pages differently:
1123  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1124  // Some pages are not valid targets.
1125  if ( $rt->isValidRedirectTarget() ) {
1126  return $rt->getFullURL();
1127  } else {
1128  return false;
1129  }
1130  }
1131 
1132  return $rt;
1133  }
1134 
1140  public function getContributors() {
1141  // @todo: This is expensive; cache this info somewhere.
1142 
1143  $dbr = wfGetDB( DB_REPLICA );
1144 
1145  $actorMigration = ActorMigration::newMigration();
1146  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1147 
1148  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1149 
1150  $fields = [
1151  'user_id' => $actorQuery['fields']['rev_user'],
1152  'user_name' => $actorQuery['fields']['rev_user_text'],
1153  'actor_id' => $actorQuery['fields']['rev_actor'],
1154  'user_real_name' => 'MIN(user_real_name)',
1155  'timestamp' => 'MAX(rev_timestamp)',
1156  ];
1157 
1158  $conds = [ 'rev_page' => $this->getId() ];
1159 
1160  // The user who made the top revision gets credited as "this page was last edited by
1161  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1162  $user = $this->getUser()
1163  ? User::newFromId( $this->getUser() )
1164  : User::newFromName( $this->getUserText(), false );
1165  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1166 
1167  // Username hidden?
1168  $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1169 
1170  $jconds = [
1171  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1172  ] + $actorQuery['joins'];
1173 
1174  $options = [
1175  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1176  'ORDER BY' => 'timestamp DESC',
1177  ];
1178 
1179  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1180  return new UserArrayFromResult( $res );
1181  }
1182 
1190  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1191  return $parserOptions->getStubThreshold() == 0
1192  && $this->exists()
1193  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1194  && $this->getContentHandler()->isParserCacheSupported();
1195  }
1196 
1212  public function getParserOutput(
1213  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1214  ) {
1215  $useParserCache =
1216  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1217 
1218  if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1219  throw new InvalidArgumentException(
1220  'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1221  );
1222  }
1223 
1224  wfDebug( __METHOD__ .
1225  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1226  if ( $parserOptions->getStubThreshold() ) {
1227  wfIncrStats( 'pcache.miss.stub' );
1228  }
1229 
1230  if ( $useParserCache ) {
1231  $parserOutput = $this->getParserCache()
1232  ->get( $this, $parserOptions );
1233  if ( $parserOutput !== false ) {
1234  return $parserOutput;
1235  }
1236  }
1237 
1238  if ( $oldid === null || $oldid === 0 ) {
1239  $oldid = $this->getLatest();
1240  }
1241 
1242  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1243  $pool->execute();
1244 
1245  return $pool->getParserOutput();
1246  }
1247 
1253  public function doViewUpdates( User $user, $oldid = 0 ) {
1254  if ( wfReadOnly() ) {
1255  return;
1256  }
1257 
1258  // Update newtalk / watchlist notification status;
1259  // Avoid outage if the master is not reachable by using a deferred updated
1261  function () use ( $user, $oldid ) {
1262  Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1263 
1264  $user->clearNotification( $this->mTitle, $oldid );
1265  },
1267  );
1268  }
1269 
1276  public function doPurge() {
1277  // Avoid PHP 7.1 warning of passing $this by reference
1278  $wikiPage = $this;
1279 
1280  if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1281  return false;
1282  }
1283 
1284  $this->mTitle->invalidateCache();
1285 
1286  // Clear file cache
1288  // Send purge after above page_touched update was committed
1290  new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1292  );
1293 
1294  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1295  $messageCache = MessageCache::singleton();
1296  $messageCache->updateMessageOverride( $this->mTitle, $this->getContent() );
1297  }
1298 
1299  return true;
1300  }
1301 
1318  public function insertOn( $dbw, $pageId = null ) {
1319  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1320  $dbw->insert(
1321  'page',
1322  [
1323  'page_namespace' => $this->mTitle->getNamespace(),
1324  'page_title' => $this->mTitle->getDBkey(),
1325  'page_restrictions' => '',
1326  'page_is_redirect' => 0, // Will set this shortly...
1327  'page_is_new' => 1,
1328  'page_random' => wfRandom(),
1329  'page_touched' => $dbw->timestamp(),
1330  'page_latest' => 0, // Fill this in shortly...
1331  'page_len' => 0, // Fill this in shortly...
1332  ] + $pageIdForInsert,
1333  __METHOD__,
1334  'IGNORE'
1335  );
1336 
1337  if ( $dbw->affectedRows() > 0 ) {
1338  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1339  $this->mId = $newid;
1340  $this->mTitle->resetArticleID( $newid );
1341 
1342  return $newid;
1343  } else {
1344  return false; // nothing changed
1345  }
1346  }
1347 
1363  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1364  $lastRevIsRedirect = null
1365  ) {
1366  global $wgContentHandlerUseDB;
1367 
1368  // TODO: move into PageUpdater or PageStore
1369  // NOTE: when doing that, make sure cached fields get reset in doEditContent,
1370  // and in the compat stub!
1371 
1372  // Assertion to try to catch T92046
1373  if ( (int)$revision->getId() === 0 ) {
1374  throw new InvalidArgumentException(
1375  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1376  );
1377  }
1378 
1379  $content = $revision->getContent();
1380  $len = $content ? $content->getSize() : 0;
1381  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1382 
1383  $conditions = [ 'page_id' => $this->getId() ];
1384 
1385  if ( !is_null( $lastRevision ) ) {
1386  // An extra check against threads stepping on each other
1387  $conditions['page_latest'] = $lastRevision;
1388  }
1389 
1390  $revId = $revision->getId();
1391  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1392 
1393  $row = [ /* SET */
1394  'page_latest' => $revId,
1395  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1396  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1397  'page_is_redirect' => $rt !== null ? 1 : 0,
1398  'page_len' => $len,
1399  ];
1400 
1401  if ( $wgContentHandlerUseDB ) {
1402  $row['page_content_model'] = $revision->getContentModel();
1403  }
1404 
1405  $dbw->update( 'page',
1406  $row,
1407  $conditions,
1408  __METHOD__ );
1409 
1410  $result = $dbw->affectedRows() > 0;
1411  if ( $result ) {
1412  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1413  $this->setLastEdit( $revision );
1414  $this->mLatest = $revision->getId();
1415  $this->mIsRedirect = (bool)$rt;
1416  // Update the LinkCache.
1417  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1418  $linkCache->addGoodLinkObj(
1419  $this->getId(),
1420  $this->mTitle,
1421  $len,
1422  $this->mIsRedirect,
1423  $this->mLatest,
1424  $revision->getContentModel()
1425  );
1426  }
1427 
1428  return $result;
1429  }
1430 
1442  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1443  // Always update redirects (target link might have changed)
1444  // Update/Insert if we don't know if the last revision was a redirect or not
1445  // Delete if changing from redirect to non-redirect
1446  $isRedirect = !is_null( $redirectTitle );
1447 
1448  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1449  return true;
1450  }
1451 
1452  if ( $isRedirect ) {
1453  $this->insertRedirectEntry( $redirectTitle );
1454  } else {
1455  // This is not a redirect, remove row from redirect table
1456  $where = [ 'rd_from' => $this->getId() ];
1457  $dbw->delete( 'redirect', $where, __METHOD__ );
1458  }
1459 
1460  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1461  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1462  }
1463 
1464  return ( $dbw->affectedRows() != 0 );
1465  }
1466 
1477  public function updateIfNewerOn( $dbw, $revision ) {
1478  $row = $dbw->selectRow(
1479  [ 'revision', 'page' ],
1480  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1481  [
1482  'page_id' => $this->getId(),
1483  'page_latest=rev_id' ],
1484  __METHOD__ );
1485 
1486  if ( $row ) {
1487  if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1488  return false;
1489  }
1490  $prev = $row->rev_id;
1491  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1492  } else {
1493  // No or missing previous revision; mark the page as new
1494  $prev = 0;
1495  $lastRevIsRedirect = null;
1496  }
1497 
1498  $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1499 
1500  return $ret;
1501  }
1502 
1515  public static function hasDifferencesOutsideMainSlot( Revision $a, Revision $b ) {
1516  $aSlots = $a->getRevisionRecord()->getSlots();
1517  $bSlots = $b->getRevisionRecord()->getSlots();
1518  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1519 
1520  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1521  }
1522 
1534  public function getUndoContent( Revision $undo, Revision $undoafter ) {
1535  // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1536 
1537  if ( self::hasDifferencesOutsideMainSlot( $undo, $undoafter ) ) {
1538  // Cannot yet undo edits that involve anything other the main slot.
1539  return false;
1540  }
1541 
1542  $handler = $undo->getContentHandler();
1543  return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1544  }
1545 
1556  public function supportsSections() {
1557  return $this->getContentHandler()->supportsSections();
1558  }
1559 
1574  public function replaceSectionContent(
1575  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1576  ) {
1577  $baseRevId = null;
1578  if ( $edittime && $sectionId !== 'new' ) {
1579  $lb = $this->getDBLoadBalancer();
1580  $dbr = $lb->getConnection( DB_REPLICA );
1581  $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1582  // Try the master if this thread may have just added it.
1583  // This could be abstracted into a Revision method, but we don't want
1584  // to encourage loading of revisions by timestamp.
1585  if ( !$rev
1586  && $lb->getServerCount() > 1
1587  && $lb->hasOrMadeRecentMasterChanges()
1588  ) {
1589  $dbw = $lb->getConnection( DB_MASTER );
1590  $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1591  }
1592  if ( $rev ) {
1593  $baseRevId = $rev->getId();
1594  }
1595  }
1596 
1597  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1598  }
1599 
1613  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1614  $sectionTitle = '', $baseRevId = null
1615  ) {
1616  if ( strval( $sectionId ) === '' ) {
1617  // Whole-page edit; let the whole text through
1618  $newContent = $sectionContent;
1619  } else {
1620  if ( !$this->supportsSections() ) {
1621  throw new MWException( "sections not supported for content model " .
1622  $this->getContentHandler()->getModelID() );
1623  }
1624 
1625  // T32711: always use current version when adding a new section
1626  if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1627  $oldContent = $this->getContent();
1628  } else {
1629  $rev = Revision::newFromId( $baseRevId );
1630  if ( !$rev ) {
1631  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1632  $this->getId() . "; section: $sectionId)\n" );
1633  return null;
1634  }
1635 
1636  $oldContent = $rev->getContent();
1637  }
1638 
1639  if ( !$oldContent ) {
1640  wfDebug( __METHOD__ . ": no page text\n" );
1641  return null;
1642  }
1643 
1644  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1645  }
1646 
1647  return $newContent;
1648  }
1649 
1659  public function checkFlags( $flags ) {
1660  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1661  if ( $this->exists() ) {
1662  $flags |= EDIT_UPDATE;
1663  } else {
1664  $flags |= EDIT_NEW;
1665  }
1666  }
1667 
1668  return $flags;
1669  }
1670 
1674  private function newDerivedDataUpdater() {
1676 
1678  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1679  $this->getRevisionStore(),
1680  $this->getRevisionRenderer(),
1681  $this->getSlotRoleRegistry(),
1682  $this->getParserCache(),
1685  MediaWikiServices::getInstance()->getContentLanguage(),
1686  MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
1687  );
1688 
1689  $derivedDataUpdater->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership );
1690  $derivedDataUpdater->setArticleCountMethod( $wgArticleCountMethod );
1691 
1692  return $derivedDataUpdater;
1693  }
1694 
1722  private function getDerivedDataUpdater(
1723  User $forUser = null,
1724  RevisionRecord $forRevision = null,
1725  RevisionSlotsUpdate $forUpdate = null,
1726  $forEdit = false
1727  ) {
1728  if ( !$forRevision && !$forUpdate ) {
1729  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1730  // going to use it with.
1731  $this->derivedDataUpdater = null;
1732  }
1733 
1734  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1735  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1736  // to it did not yet initialize it, because we don't know what data it will be
1737  // initialized with.
1738  $this->derivedDataUpdater = null;
1739  }
1740 
1741  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1742  // However, there is no good way to construct a cache key. We'd need to check against all
1743  // cached instances.
1744 
1745  if ( $this->derivedDataUpdater
1746  && !$this->derivedDataUpdater->isReusableFor(
1747  $forUser,
1748  $forRevision,
1749  $forUpdate,
1750  $forEdit ? $this->getLatest() : null
1751  )
1752  ) {
1753  $this->derivedDataUpdater = null;
1754  }
1755 
1756  if ( !$this->derivedDataUpdater ) {
1757  $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1758  }
1759 
1761  }
1762 
1778  public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1780 
1781  $pageUpdater = new PageUpdater(
1782  $user,
1783  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1784  $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1785  $this->getDBLoadBalancer(),
1786  $this->getRevisionStore(),
1787  $this->getSlotRoleRegistry()
1788  );
1789 
1790  $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
1791  $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
1792  $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
1793 
1794  return $pageUpdater;
1795  }
1796 
1859  public function doEditContent(
1860  Content $content, $summary, $flags = 0, $originalRevId = false,
1861  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1862  ) {
1863  global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1864 
1865  if ( !( $summary instanceof CommentStoreComment ) ) {
1866  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1867  }
1868 
1869  if ( !$user ) {
1870  $user = $wgUser;
1871  }
1872 
1873  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1874  // Checking the minoredit right should be done in the same place the 'bot' right is
1875  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1876  if ( ( $flags & EDIT_MINOR ) && !$user->isAllowed( 'minoredit' ) ) {
1877  $flags = ( $flags & ~EDIT_MINOR );
1878  }
1879 
1880  $slotsUpdate = new RevisionSlotsUpdate();
1881  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1882 
1883  // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1884  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1885  // used by this PageUpdater. However, there is no guarantee for this.
1886  $updater = $this->newPageUpdater( $user, $slotsUpdate );
1887  $updater->setContent( SlotRecord::MAIN, $content );
1888  $updater->setOriginalRevisionId( $originalRevId );
1889  $updater->setUndidRevisionId( $undidRevId );
1890 
1891  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1892 
1893  // TODO: this logic should not be in the storage layer, it's here for compatibility
1894  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1895  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1896  if ( $needsPatrol && $this->getTitle()->userCan( 'autopatrol', $user ) ) {
1897  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1898  }
1899 
1900  $updater->addTags( $tags );
1901 
1902  $revRec = $updater->saveRevision(
1903  $summary,
1904  $flags
1905  );
1906 
1907  // $revRec will be null if the edit failed, or if no new revision was created because
1908  // the content did not change.
1909  if ( $revRec ) {
1910  // update cached fields
1911  // TODO: this is currently redundant to what is done in updateRevisionOn.
1912  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1913  $this->setLastEdit( new Revision( $revRec ) ); // TODO: use RevisionRecord
1914  $this->mLatest = $revRec->getId();
1915  }
1916 
1917  return $updater->getStatus();
1918  }
1919 
1934  public function makeParserOptions( $context ) {
1936 
1937  if ( $this->getTitle()->isConversionTable() ) {
1938  // @todo ConversionTable should become a separate content model, so
1939  // we don't need special cases like this one.
1940  $options->disableContentConversion();
1941  }
1942 
1943  return $options;
1944  }
1945 
1966  public function prepareContentForEdit(
1967  Content $content,
1968  $revision = null,
1969  User $user = null,
1970  $serialFormat = null,
1971  $useCache = true
1972  ) {
1973  global $wgUser;
1974 
1975  if ( !$user ) {
1976  $user = $wgUser;
1977  }
1978 
1979  if ( !is_object( $revision ) ) {
1980  $revid = $revision;
1981  // This code path is deprecated, and nothing is known to
1982  // use it, so performance here shouldn't be a worry.
1983  if ( $revid !== null ) {
1984  wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
1985  $store = $this->getRevisionStore();
1986  $revision = $store->getRevisionById( $revid, Revision::READ_LATEST );
1987  } else {
1988  $revision = null;
1989  }
1990  } elseif ( $revision instanceof Revision ) {
1991  $revision = $revision->getRevisionRecord();
1992  }
1993 
1994  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
1995  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
1996 
1997  if ( !$updater->isUpdatePrepared() ) {
1998  $updater->prepareContent( $user, $slots, $useCache );
1999 
2000  if ( $revision ) {
2001  $updater->prepareUpdate(
2002  $revision,
2003  [
2004  'causeAction' => 'prepare-edit',
2005  'causeAgent' => $user->getName(),
2006  ]
2007  );
2008  }
2009  }
2010 
2011  return $updater->getPreparedEdit();
2012  }
2013 
2041  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2042  $options += [
2043  'causeAction' => 'edit-page',
2044  'causeAgent' => $user->getName(),
2045  ];
2046 
2047  $revision = $revision->getRevisionRecord();
2048 
2049  $updater = $this->getDerivedDataUpdater( $user, $revision );
2050 
2051  $updater->prepareUpdate( $revision, $options );
2052 
2053  $updater->doUpdates();
2054  }
2055 
2069  public function updateParserCache( array $options = [] ) {
2070  $revision = $this->getRevisionRecord();
2071  if ( !$revision || !$revision->getId() ) {
2072  LoggerFactory::getInstance( 'wikipage' )->info(
2073  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2074  );
2075  return;
2076  }
2077  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2078 
2079  $updater = $this->getDerivedDataUpdater( $user, $revision );
2080  $updater->prepareUpdate( $revision, $options );
2081  $updater->doParserCacheUpdate();
2082  }
2083 
2110  public function doSecondaryDataUpdates( array $options = [] ) {
2111  $options['recursive'] = $options['recursive'] ?? true;
2112  $revision = $this->getRevisionRecord();
2113  if ( !$revision || !$revision->getId() ) {
2114  LoggerFactory::getInstance( 'wikipage' )->info(
2115  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2116  );
2117  return;
2118  }
2119  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2120 
2121  $updater = $this->getDerivedDataUpdater( $user, $revision );
2122  $updater->prepareUpdate( $revision, $options );
2123  $updater->doSecondaryDataUpdates( $options );
2124  }
2125 
2140  public function doUpdateRestrictions( array $limit, array $expiry,
2141  &$cascade, $reason, User $user, $tags = null
2142  ) {
2144 
2145  if ( wfReadOnly() ) {
2146  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2147  }
2148 
2149  $this->loadPageData( 'fromdbmaster' );
2150  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2151  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2152  $id = $this->getId();
2153 
2154  if ( !$cascade ) {
2155  $cascade = false;
2156  }
2157 
2158  // Take this opportunity to purge out expired restrictions
2160 
2161  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2162  // we expect a single selection, but the schema allows otherwise.
2163  $isProtected = false;
2164  $protect = false;
2165  $changed = false;
2166 
2167  $dbw = wfGetDB( DB_MASTER );
2168 
2169  foreach ( $restrictionTypes as $action ) {
2170  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2171  $expiry[$action] = 'infinity';
2172  }
2173  if ( !isset( $limit[$action] ) ) {
2174  $limit[$action] = '';
2175  } elseif ( $limit[$action] != '' ) {
2176  $protect = true;
2177  }
2178 
2179  // Get current restrictions on $action
2180  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2181  if ( $current != '' ) {
2182  $isProtected = true;
2183  }
2184 
2185  if ( $limit[$action] != $current ) {
2186  $changed = true;
2187  } elseif ( $limit[$action] != '' ) {
2188  // Only check expiry change if the action is actually being
2189  // protected, since expiry does nothing on an not-protected
2190  // action.
2191  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2192  $changed = true;
2193  }
2194  }
2195  }
2196 
2197  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2198  $changed = true;
2199  }
2200 
2201  // If nothing has changed, do nothing
2202  if ( !$changed ) {
2203  return Status::newGood();
2204  }
2205 
2206  if ( !$protect ) { // No protection at all means unprotection
2207  $revCommentMsg = 'unprotectedarticle-comment';
2208  $logAction = 'unprotect';
2209  } elseif ( $isProtected ) {
2210  $revCommentMsg = 'modifiedarticleprotection-comment';
2211  $logAction = 'modify';
2212  } else {
2213  $revCommentMsg = 'protectedarticle-comment';
2214  $logAction = 'protect';
2215  }
2216 
2217  $logRelationsValues = [];
2218  $logRelationsField = null;
2219  $logParamsDetails = [];
2220 
2221  // Null revision (used for change tag insertion)
2222  $nullRevision = null;
2223 
2224  if ( $id ) { // Protection of existing page
2225  // Avoid PHP 7.1 warning of passing $this by reference
2226  $wikiPage = $this;
2227 
2228  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2229  return Status::newGood();
2230  }
2231 
2232  // Only certain restrictions can cascade...
2233  $editrestriction = isset( $limit['edit'] )
2234  ? [ $limit['edit'] ]
2235  : $this->mTitle->getRestrictions( 'edit' );
2236  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2237  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2238  }
2239  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2240  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2241  }
2242 
2243  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2244  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2245  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2246  }
2247  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2248  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2249  }
2250 
2251  // The schema allows multiple restrictions
2252  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2253  $cascade = false;
2254  }
2255 
2256  // insert null revision to identify the page protection change as edit summary
2257  $latest = $this->getLatest();
2258  $nullRevision = $this->insertProtectNullRevision(
2259  $revCommentMsg,
2260  $limit,
2261  $expiry,
2262  $cascade,
2263  $reason,
2264  $user
2265  );
2266 
2267  if ( $nullRevision === null ) {
2268  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2269  }
2270 
2271  $logRelationsField = 'pr_id';
2272 
2273  // Update restrictions table
2274  foreach ( $limit as $action => $restrictions ) {
2275  $dbw->delete(
2276  'page_restrictions',
2277  [
2278  'pr_page' => $id,
2279  'pr_type' => $action
2280  ],
2281  __METHOD__
2282  );
2283  if ( $restrictions != '' ) {
2284  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2285  $dbw->insert(
2286  'page_restrictions',
2287  [
2288  'pr_page' => $id,
2289  'pr_type' => $action,
2290  'pr_level' => $restrictions,
2291  'pr_cascade' => $cascadeValue,
2292  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2293  ],
2294  __METHOD__
2295  );
2296  $logRelationsValues[] = $dbw->insertId();
2297  $logParamsDetails[] = [
2298  'type' => $action,
2299  'level' => $restrictions,
2300  'expiry' => $expiry[$action],
2301  'cascade' => (bool)$cascadeValue,
2302  ];
2303  }
2304  }
2305 
2306  // Clear out legacy restriction fields
2307  $dbw->update(
2308  'page',
2309  [ 'page_restrictions' => '' ],
2310  [ 'page_id' => $id ],
2311  __METHOD__
2312  );
2313 
2314  // Avoid PHP 7.1 warning of passing $this by reference
2315  $wikiPage = $this;
2316 
2317  Hooks::run( 'NewRevisionFromEditComplete',
2318  [ $this, $nullRevision, $latest, $user ] );
2319  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2320  } else { // Protection of non-existing page (also known as "title protection")
2321  // Cascade protection is meaningless in this case
2322  $cascade = false;
2323 
2324  if ( $limit['create'] != '' ) {
2325  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2326  $dbw->replace( 'protected_titles',
2327  [ [ 'pt_namespace', 'pt_title' ] ],
2328  [
2329  'pt_namespace' => $this->mTitle->getNamespace(),
2330  'pt_title' => $this->mTitle->getDBkey(),
2331  'pt_create_perm' => $limit['create'],
2332  'pt_timestamp' => $dbw->timestamp(),
2333  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2334  'pt_user' => $user->getId(),
2335  ] + $commentFields, __METHOD__
2336  );
2337  $logParamsDetails[] = [
2338  'type' => 'create',
2339  'level' => $limit['create'],
2340  'expiry' => $expiry['create'],
2341  ];
2342  } else {
2343  $dbw->delete( 'protected_titles',
2344  [
2345  'pt_namespace' => $this->mTitle->getNamespace(),
2346  'pt_title' => $this->mTitle->getDBkey()
2347  ], __METHOD__
2348  );
2349  }
2350  }
2351 
2352  $this->mTitle->flushRestrictions();
2353  InfoAction::invalidateCache( $this->mTitle );
2354 
2355  if ( $logAction == 'unprotect' ) {
2356  $params = [];
2357  } else {
2358  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2359  $params = [
2360  '4::description' => $protectDescriptionLog, // parameter for IRC
2361  '5:bool:cascade' => $cascade,
2362  'details' => $logParamsDetails, // parameter for localize and api
2363  ];
2364  }
2365 
2366  // Update the protection log
2367  $logEntry = new ManualLogEntry( 'protect', $logAction );
2368  $logEntry->setTarget( $this->mTitle );
2369  $logEntry->setComment( $reason );
2370  $logEntry->setPerformer( $user );
2371  $logEntry->setParameters( $params );
2372  if ( !is_null( $nullRevision ) ) {
2373  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2374  }
2375  $logEntry->setTags( $tags );
2376  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2377  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2378  }
2379  $logId = $logEntry->insert();
2380  $logEntry->publish( $logId );
2381 
2382  return Status::newGood( $logId );
2383  }
2384 
2396  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2397  array $expiry, $cascade, $reason, $user = null
2398  ) {
2399  $dbw = wfGetDB( DB_MASTER );
2400 
2401  // Prepare a null revision to be added to the history
2402  $editComment = wfMessage(
2403  $revCommentMsg,
2404  $this->mTitle->getPrefixedText(),
2405  $user ? $user->getName() : ''
2406  )->inContentLanguage()->text();
2407  if ( $reason ) {
2408  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2409  }
2410  $protectDescription = $this->protectDescription( $limit, $expiry );
2411  if ( $protectDescription ) {
2412  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2413  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2414  ->inContentLanguage()->text();
2415  }
2416  if ( $cascade ) {
2417  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2418  $editComment .= wfMessage( 'brackets' )->params(
2419  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2420  )->inContentLanguage()->text();
2421  }
2422 
2423  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2424  if ( $nullRev ) {
2425  $nullRev->insertOn( $dbw );
2426 
2427  // Update page record and touch page
2428  $oldLatest = $nullRev->getParentId();
2429  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2430  }
2431 
2432  return $nullRev;
2433  }
2434 
2439  protected function formatExpiry( $expiry ) {
2440  if ( $expiry != 'infinity' ) {
2441  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2442  return wfMessage(
2443  'protect-expiring',
2444  $contLang->timeanddate( $expiry, false, false ),
2445  $contLang->date( $expiry, false, false ),
2446  $contLang->time( $expiry, false, false )
2447  )->inContentLanguage()->text();
2448  } else {
2449  return wfMessage( 'protect-expiry-indefinite' )
2450  ->inContentLanguage()->text();
2451  }
2452  }
2453 
2461  public function protectDescription( array $limit, array $expiry ) {
2462  $protectDescription = '';
2463 
2464  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2465  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2466  # All possible message keys are listed here for easier grepping:
2467  # * restriction-create
2468  # * restriction-edit
2469  # * restriction-move
2470  # * restriction-upload
2471  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2472  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2473  # with '' filtered out. All possible message keys are listed below:
2474  # * protect-level-autoconfirmed
2475  # * protect-level-sysop
2476  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2477  ->inContentLanguage()->text();
2478 
2479  $expiryText = $this->formatExpiry( $expiry[$action] );
2480 
2481  if ( $protectDescription !== '' ) {
2482  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2483  }
2484  $protectDescription .= wfMessage( 'protect-summary-desc' )
2485  ->params( $actionText, $restrictionsText, $expiryText )
2486  ->inContentLanguage()->text();
2487  }
2488 
2489  return $protectDescription;
2490  }
2491 
2503  public function protectDescriptionLog( array $limit, array $expiry ) {
2504  $protectDescriptionLog = '';
2505 
2506  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2507  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2508  $expiryText = $this->formatExpiry( $expiry[$action] );
2509  $protectDescriptionLog .=
2510  $dirMark .
2511  "[$action=$restrictions] ($expiryText)";
2512  }
2513 
2514  return trim( $protectDescriptionLog );
2515  }
2516 
2526  protected static function flattenRestrictions( $limit ) {
2527  if ( !is_array( $limit ) ) {
2528  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2529  }
2530 
2531  $bits = [];
2532  ksort( $limit );
2533 
2534  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2535  $bits[] = "$action=$restrictions";
2536  }
2537 
2538  return implode( ':', $bits );
2539  }
2540 
2553  public function isBatchedDelete( $safetyMargin = 0 ) {
2555 
2556  $dbr = wfGetDB( DB_REPLICA );
2557  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2558  $revCount += $safetyMargin;
2559 
2560  return $revCount >= $wgDeleteRevisionsBatchSize;
2561  }
2562 
2582  public function doDeleteArticle(
2583  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2584  $immediate = false
2585  ) {
2586  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2587  [], 'delete', $immediate );
2588 
2589  // Returns true if the page was actually deleted, or is scheduled for deletion
2590  return $status->isOK();
2591  }
2592 
2615  public function doDeleteArticleReal(
2616  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2617  $tags = [], $logsubtype = 'delete', $immediate = false
2618  ) {
2619  global $wgUser;
2620 
2621  wfDebug( __METHOD__ . "\n" );
2622 
2624 
2625  // Avoid PHP 7.1 warning of passing $this by reference
2626  $wikiPage = $this;
2627 
2628  $deleter = is_null( $deleter ) ? $wgUser : $deleter;
2629  if ( !Hooks::run( 'ArticleDelete',
2630  [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2631  ) ) {
2632  if ( $status->isOK() ) {
2633  // Hook aborted but didn't set a fatal status
2634  $status->fatal( 'delete-hook-aborted' );
2635  }
2636  return $status;
2637  }
2638 
2639  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2640  $logsubtype, $immediate );
2641  }
2642 
2651  public function doDeleteArticleBatched(
2652  $reason, $suppress, User $deleter, $tags,
2653  $logsubtype, $immediate = false, $webRequestId = null
2654  ) {
2655  wfDebug( __METHOD__ . "\n" );
2656 
2658 
2659  $dbw = wfGetDB( DB_MASTER );
2660  $dbw->startAtomic( __METHOD__ );
2661 
2662  $this->loadPageData( self::READ_LATEST );
2663  $id = $this->getId();
2664  // T98706: lock the page from various other updates but avoid using
2665  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2666  // the revisions queries (which also JOIN on user). Only lock the page
2667  // row and CAS check on page_latest to see if the trx snapshot matches.
2668  $lockedLatest = $this->lockAndGetLatest();
2669  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2670  $dbw->endAtomic( __METHOD__ );
2671  // Page not there or trx snapshot is stale
2672  $status->error( 'cannotdelete',
2673  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2674  return $status;
2675  }
2676 
2677  // At this point we are now committed to returning an OK
2678  // status unless some DB query error or other exception comes up.
2679  // This way callers don't have to call rollback() if $status is bad
2680  // unless they actually try to catch exceptions (which is rare).
2681 
2682  // we need to remember the old content so we can use it to generate all deletion updates.
2683  $revision = $this->getRevision();
2684  try {
2685  $content = $this->getContent( Revision::RAW );
2686  } catch ( Exception $ex ) {
2687  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2688  . $ex->getMessage() );
2689 
2690  $content = null;
2691  }
2692 
2693  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2694  // one batch of revisions and defer archival of any others to the job queue.
2695  $explictTrxLogged = false;
2696  while ( true ) {
2697  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2698  if ( $done || !$immediate ) {
2699  break;
2700  }
2701  $dbw->endAtomic( __METHOD__ );
2702  if ( $dbw->explicitTrxActive() ) {
2703  // Explict transactions may never happen here in practice. Log to be sure.
2704  if ( !$explictTrxLogged ) {
2705  $explictTrxLogged = true;
2706  LoggerFactory::getInstance( 'wfDebug' )->debug(
2707  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2708  'title' => $this->getTitle()->getText(),
2709  ] );
2710  }
2711  continue;
2712  }
2713  if ( $dbw->trxLevel() ) {
2714  $dbw->commit();
2715  }
2716  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2717  $lbFactory->waitForReplication();
2718  $dbw->startAtomic( __METHOD__ );
2719  }
2720 
2721  // If done archiving, also delete the article.
2722  if ( !$done ) {
2723  $dbw->endAtomic( __METHOD__ );
2724 
2725  $jobParams = [
2726  'wikiPageId' => $id,
2727  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2728  'reason' => $reason,
2729  'suppress' => $suppress,
2730  'userId' => $deleter->getId(),
2731  'tags' => json_encode( $tags ),
2732  'logsubtype' => $logsubtype,
2733  ];
2734 
2735  $job = new DeletePageJob( $this->getTitle(), $jobParams );
2736  JobQueueGroup::singleton()->push( $job );
2737 
2738  $status->warning( 'delete-scheduled',
2739  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2740  } else {
2741  // Get archivedRevisionCount by db query, because there's no better alternative.
2742  // Jobs cannot pass a count of archived revisions to the next job, because additional
2743  // deletion operations can be started while the first is running. Jobs from each
2744  // gracefully interleave, but would not know about each other's count. Deduplication
2745  // in the job queue to avoid simultaneous deletion operations would add overhead.
2746  // Number of archived revisions cannot be known beforehand, because edits can be made
2747  // while deletion operations are being processed, changing the number of archivals.
2748  $archivedRevisionCount = (int)$dbw->selectField(
2749  'archive', 'COUNT(*)',
2750  [
2751  'ar_namespace' => $this->getTitle()->getNamespace(),
2752  'ar_title' => $this->getTitle()->getDBkey(),
2753  'ar_page_id' => $id
2754  ], __METHOD__
2755  );
2756 
2757  // Clone the title and wikiPage, so we have the information we need when
2758  // we log and run the ArticleDeleteComplete hook.
2759  $logTitle = clone $this->mTitle;
2760  $wikiPageBeforeDelete = clone $this;
2761 
2762  // Now that it's safely backed up, delete it
2763  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2764 
2765  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2766  $logtype = $suppress ? 'suppress' : 'delete';
2767 
2768  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2769  $logEntry->setPerformer( $deleter );
2770  $logEntry->setTarget( $logTitle );
2771  $logEntry->setComment( $reason );
2772  $logEntry->setTags( $tags );
2773  $logid = $logEntry->insert();
2774 
2775  $dbw->onTransactionPreCommitOrIdle(
2776  function () use ( $logEntry, $logid ) {
2777  // T58776: avoid deadlocks (especially from FileDeleteForm)
2778  $logEntry->publish( $logid );
2779  },
2780  __METHOD__
2781  );
2782 
2783  $dbw->endAtomic( __METHOD__ );
2784 
2785  $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2786 
2787  Hooks::run( 'ArticleDeleteComplete', [
2788  &$wikiPageBeforeDelete,
2789  &$deleter,
2790  $reason,
2791  $id,
2792  $content,
2793  $logEntry,
2794  $archivedRevisionCount
2795  ] );
2796  $status->value = $logid;
2797 
2798  // Show log excerpt on 404 pages rather than just a link
2799  $cache = MediaWikiServices::getInstance()->getMainObjectStash();
2800  $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2801  $cache->set( $key, 1, $cache::TTL_DAY );
2802  }
2803 
2804  return $status;
2805  }
2806 
2816  protected function archiveRevisions( $dbw, $id, $suppress ) {
2820 
2821  // Given the lock above, we can be confident in the title and page ID values
2822  $namespace = $this->getTitle()->getNamespace();
2823  $dbKey = $this->getTitle()->getDBkey();
2824 
2825  $commentStore = CommentStore::getStore();
2826  $actorMigration = ActorMigration::newMigration();
2827 
2829  $bitfield = false;
2830 
2831  // Bitfields to further suppress the content
2832  if ( $suppress ) {
2833  $bitfield = Revision::SUPPRESSED_ALL;
2834  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2835  }
2836 
2837  // For now, shunt the revision data into the archive table.
2838  // Text is *not* removed from the text table; bulk storage
2839  // is left intact to avoid breaking block-compression or
2840  // immutable storage schemes.
2841  // In the future, we may keep revisions and mark them with
2842  // the rev_deleted field, which is reserved for this purpose.
2843 
2844  // Lock rows in `revision` and its temp tables, but not any others.
2845  // Note array_intersect() preserves keys from the first arg, and we're
2846  // assuming $revQuery has `revision` primary and isn't using subtables
2847  // for anything we care about.
2848  $dbw->lockForUpdate(
2849  array_intersect(
2850  $revQuery['tables'],
2851  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2852  ),
2853  [ 'rev_page' => $id ],
2854  __METHOD__,
2855  [],
2856  $revQuery['joins']
2857  );
2858 
2859  // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
2860  // so we can copy it to the archive table.
2861  // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
2862  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2863  $revQuery['fields'][] = 'rev_text_id';
2864 
2865  if ( $wgContentHandlerUseDB ) {
2866  $revQuery['fields'][] = 'rev_content_model';
2867  $revQuery['fields'][] = 'rev_content_format';
2868  }
2869  }
2870 
2871  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2872  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2873  $res = $dbw->select(
2874  $revQuery['tables'],
2875  $revQuery['fields'],
2876  [ 'rev_page' => $id ],
2877  __METHOD__,
2878  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2879  $revQuery['joins']
2880  );
2881 
2882  // Build their equivalent archive rows
2883  $rowsInsert = [];
2884  $revids = [];
2885 
2887  $ipRevIds = [];
2888 
2889  $done = true;
2890  foreach ( $res as $row ) {
2891  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2892  $done = false;
2893  break;
2894  }
2895 
2896  $comment = $commentStore->getComment( 'rev_comment', $row );
2897  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2898  $rowInsert = [
2899  'ar_namespace' => $namespace,
2900  'ar_title' => $dbKey,
2901  'ar_timestamp' => $row->rev_timestamp,
2902  'ar_minor_edit' => $row->rev_minor_edit,
2903  'ar_rev_id' => $row->rev_id,
2904  'ar_parent_id' => $row->rev_parent_id,
2913  'ar_len' => $row->rev_len,
2914  'ar_page_id' => $id,
2915  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2916  'ar_sha1' => $row->rev_sha1,
2917  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2918  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2919 
2920  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2921  $rowInsert['ar_text_id'] = $row->rev_text_id;
2922 
2923  if ( $wgContentHandlerUseDB ) {
2924  $rowInsert['ar_content_model'] = $row->rev_content_model;
2925  $rowInsert['ar_content_format'] = $row->rev_content_format;
2926  }
2927  }
2928 
2929  $rowsInsert[] = $rowInsert;
2930  $revids[] = $row->rev_id;
2931 
2932  // Keep track of IP edits, so that the corresponding rows can
2933  // be deleted in the ip_changes table.
2934  if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2935  $ipRevIds[] = $row->rev_id;
2936  }
2937  }
2938 
2939  // This conditional is just a sanity check
2940  if ( count( $revids ) > 0 ) {
2941  // Copy them into the archive table
2942  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2943 
2944  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2945  if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
2946  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2947  }
2948  if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
2949  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2950  }
2951 
2952  // Also delete records from ip_changes as applicable.
2953  if ( count( $ipRevIds ) > 0 ) {
2954  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2955  }
2956  }
2957 
2958  return $done;
2959  }
2960 
2967  public function lockAndGetLatest() {
2968  return (int)wfGetDB( DB_MASTER )->selectField(
2969  'page',
2970  'page_latest',
2971  [
2972  'page_id' => $this->getId(),
2973  // Typically page_id is enough, but some code might try to do
2974  // updates assuming the title is the same, so verify that
2975  'page_namespace' => $this->getTitle()->getNamespace(),
2976  'page_title' => $this->getTitle()->getDBkey()
2977  ],
2978  __METHOD__,
2979  [ 'FOR UPDATE' ]
2980  );
2981  }
2982 
2995  public function doDeleteUpdates(
2996  $id, Content $content = null, Revision $revision = null, User $user = null
2997  ) {
2998  if ( $id !== $this->getId() ) {
2999  throw new InvalidArgumentException( 'Mismatching page ID' );
3000  }
3001 
3002  try {
3003  $countable = $this->isCountable();
3004  } catch ( Exception $ex ) {
3005  // fallback for deleting broken pages for which we cannot load the content for
3006  // some reason. Note that doDeleteArticleReal() already logged this problem.
3007  $countable = false;
3008  }
3009 
3010  // Update site status
3012  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3013  ) );
3014 
3015  // Delete pagelinks, update secondary indexes, etc
3016  $updates = $this->getDeletionUpdates(
3017  $revision ? $revision->getRevisionRecord() : $content
3018  );
3019  foreach ( $updates as $update ) {
3020  DeferredUpdates::addUpdate( $update );
3021  }
3022 
3023  $causeAgent = $user ? $user->getName() : 'unknown';
3024  // Reparse any pages transcluding this page
3026  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3027  // Reparse any pages including this image
3028  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3030  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3031  }
3032 
3033  // Clear caches
3034  self::onArticleDelete( $this->mTitle );
3036  $this->mTitle, $revision, null, wfWikiID()
3037  );
3038 
3039  // Reset this object and the Title object
3040  $this->loadFromRow( false, self::READ_LATEST );
3041 
3042  // Search engine
3043  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3044  }
3045 
3075  public function doRollback(
3076  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3077  ) {
3078  $resultDetails = null;
3079 
3080  // Check permissions
3081  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3082  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3083  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3084 
3085  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3086  $errors[] = [ 'sessionfailure' ];
3087  }
3088 
3089  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3090  $errors[] = [ 'actionthrottledtext' ];
3091  }
3092 
3093  // If there were errors, bail out now
3094  if ( !empty( $errors ) ) {
3095  return $errors;
3096  }
3097 
3098  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3099  }
3100 
3121  public function commitRollback( $fromP, $summary, $bot,
3122  &$resultDetails, User $guser, $tags = null
3123  ) {
3124  global $wgUseRCPatrol;
3125 
3126  $dbw = wfGetDB( DB_MASTER );
3127 
3128  if ( wfReadOnly() ) {
3129  return [ [ 'readonlytext' ] ];
3130  }
3131 
3132  // Begin revision creation cycle by creating a PageUpdater.
3133  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3134  $updater = $this->newPageUpdater( $guser );
3135  $current = $updater->grabParentRevision();
3136 
3137  if ( is_null( $current ) ) {
3138  // Something wrong... no page?
3139  return [ [ 'notanarticle' ] ];
3140  }
3141 
3142  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3143  $legacyCurrent = new Revision( $current );
3144  $from = str_replace( '_', ' ', $fromP );
3145 
3146  // User name given should match up with the top revision.
3147  // If the revision's user is not visible, then $from should be empty.
3148  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3149  $resultDetails = [ 'current' => $legacyCurrent ];
3150  return [ [ 'alreadyrolled',
3151  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3152  htmlspecialchars( $fromP ),
3153  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3154  ] ];
3155  }
3156 
3157  // Get the last edit not by this person...
3158  // Note: these may not be public values
3159  $actorWhere = ActorMigration::newMigration()->getWhere(
3160  $dbw,
3161  'rev_user',
3162  $current->getUser( RevisionRecord::RAW )
3163  );
3164 
3165  $s = $dbw->selectRow(
3166  [ 'revision' ] + $actorWhere['tables'],
3167  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3168  [
3169  'rev_page' => $current->getPageId(),
3170  'NOT(' . $actorWhere['conds'] . ')',
3171  ],
3172  __METHOD__,
3173  [
3174  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3175  'ORDER BY' => 'rev_timestamp DESC'
3176  ],
3177  $actorWhere['joins']
3178  );
3179  if ( $s === false ) {
3180  // No one else ever edited this page
3181  return [ [ 'cantrollback' ] ];
3182  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3183  || $s->rev_deleted & RevisionRecord::DELETED_USER
3184  ) {
3185  // Only admins can see this text
3186  return [ [ 'notvisiblerev' ] ];
3187  }
3188 
3189  // Generate the edit summary if necessary
3190  $target = $this->getRevisionStore()->getRevisionById(
3191  $s->rev_id,
3192  RevisionStore::READ_LATEST
3193  );
3194  if ( empty( $summary ) ) {
3195  if ( !$currentEditorForPublic ) { // no public user name
3196  $summary = wfMessage( 'revertpage-nouser' );
3197  } else {
3198  $summary = wfMessage( 'revertpage' );
3199  }
3200  }
3201  $legacyTarget = new Revision( $target );
3202  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3203 
3204  // Allow the custom summary to use the same args as the default message
3205  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3206  $args = [
3207  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3208  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3209  $s->rev_id,
3210  $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3211  $current->getId(),
3212  $contLang->timeanddate( $current->getTimestamp() )
3213  ];
3214  if ( $summary instanceof Message ) {
3215  $summary = $summary->params( $args )->inContentLanguage()->text();
3216  } else {
3217  $summary = wfMsgReplaceArgs( $summary, $args );
3218  }
3219 
3220  // Trim spaces on user supplied text
3221  $summary = trim( $summary );
3222 
3223  // Save
3224  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3225 
3226  if ( $guser->isAllowed( 'minoredit' ) ) {
3227  $flags |= EDIT_MINOR;
3228  }
3229 
3230  if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3231  $flags |= EDIT_FORCE_BOT;
3232  }
3233 
3234  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3235  $currentContent = $current->getContent( SlotRecord::MAIN );
3236  $targetContent = $target->getContent( SlotRecord::MAIN );
3237  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3238 
3239  if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3240  $tags[] = 'mw-rollback';
3241  }
3242 
3243  // Build rollback revision:
3244  // Restore old content
3245  // TODO: MCR: test this once we can store multiple slots
3246  foreach ( $target->getSlots()->getSlots() as $slot ) {
3247  $updater->inheritSlot( $slot );
3248  }
3249 
3250  // Remove extra slots
3251  // TODO: MCR: test this once we can store multiple slots
3252  foreach ( $current->getSlotRoles() as $role ) {
3253  if ( !$target->hasSlot( $role ) ) {
3254  $updater->removeSlot( $role );
3255  }
3256  }
3257 
3258  $updater->setOriginalRevisionId( $target->getId() );
3259  // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3260  $updater->addTags( $tags );
3261 
3262  // TODO: this logic should not be in the storage layer, it's here for compatibility
3263  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3264  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3265  if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $guser ) ) {
3266  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3267  }
3268 
3269  // Actually store the rollback
3270  $rev = $updater->saveRevision(
3272  $flags
3273  );
3274 
3275  // Set patrolling and bot flag on the edits, which gets rollbacked.
3276  // This is done even on edit failure to have patrolling in that case (T64157).
3277  $set = [];
3278  if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3279  // Mark all reverted edits as bot
3280  $set['rc_bot'] = 1;
3281  }
3282 
3283  if ( $wgUseRCPatrol ) {
3284  // Mark all reverted edits as patrolled
3285  $set['rc_patrolled'] = RecentChange::PRC_PATROLLED;
3286  }
3287 
3288  if ( count( $set ) ) {
3289  $actorWhere = ActorMigration::newMigration()->getWhere(
3290  $dbw,
3291  'rc_user',
3292  $current->getUser( RevisionRecord::RAW ),
3293  false
3294  );
3295  $dbw->update( 'recentchanges', $set,
3296  [ /* WHERE */
3297  'rc_cur_id' => $current->getPageId(),
3298  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3299  $actorWhere['conds'], // No tables/joins are needed for rc_user
3300  ],
3301  __METHOD__
3302  );
3303  }
3304 
3305  if ( !$updater->wasSuccessful() ) {
3306  return $updater->getStatus()->getErrorsArray();
3307  }
3308 
3309  // Report if the edit was not created because it did not change the content.
3310  if ( $updater->isUnchanged() ) {
3311  $resultDetails = [ 'current' => $legacyCurrent ];
3312  return [ [ 'alreadyrolled',
3313  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3314  htmlspecialchars( $fromP ),
3315  htmlspecialchars( $targetEditorForPublic ? $targetEditorForPublic->getName() : '' )
3316  ] ];
3317  }
3318 
3319  if ( $changingContentModel ) {
3320  // If the content model changed during the rollback,
3321  // make sure it gets logged to Special:Log/contentmodel
3322  $log = new ManualLogEntry( 'contentmodel', 'change' );
3323  $log->setPerformer( $guser );
3324  $log->setTarget( $this->mTitle );
3325  $log->setComment( $summary );
3326  $log->setParameters( [
3327  '4::oldmodel' => $currentContent->getModel(),
3328  '5::newmodel' => $targetContent->getModel(),
3329  ] );
3330 
3331  $logId = $log->insert( $dbw );
3332  $log->publish( $logId );
3333  }
3334 
3335  $revId = $rev->getId();
3336 
3337  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3338 
3339  $resultDetails = [
3340  'summary' => $summary,
3341  'current' => $legacyCurrent,
3342  'target' => $legacyTarget,
3343  'newid' => $revId,
3344  'tags' => $tags
3345  ];
3346 
3347  // TODO: make this return a Status object and wrap $resultDetails in that.
3348  return [];
3349  }
3350 
3362  public static function onArticleCreate( Title $title ) {
3363  // TODO: move this into a PageEventEmitter service
3364 
3365  // Update existence markers on article/talk tabs...
3366  $other = $title->getOtherPage();
3367 
3368  $other->purgeSquid();
3369 
3370  $title->touchLinks();
3371  $title->purgeSquid();
3372  $title->deleteTitleProtection();
3373 
3374  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3375 
3376  // Invalidate caches of articles which include this page
3378  new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
3379  );
3380 
3381  if ( $title->getNamespace() == NS_CATEGORY ) {
3382  // Load the Category object, which will schedule a job to create
3383  // the category table row if necessary. Checking a replica DB is ok
3384  // here, in the worst case it'll run an unnecessary recount job on
3385  // a category that probably doesn't have many members.
3386  Category::newFromTitle( $title )->getID();
3387  }
3388  }
3389 
3395  public static function onArticleDelete( Title $title ) {
3396  // TODO: move this into a PageEventEmitter service
3397 
3398  // Update existence markers on article/talk tabs...
3399  // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3400  BacklinkCache::get( $title )->clear();
3401  $other = $title->getOtherPage();
3402 
3403  $other->purgeSquid();
3404 
3405  $title->touchLinks();
3406  $title->purgeSquid();
3407 
3408  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3409 
3410  // File cache
3412  InfoAction::invalidateCache( $title );
3413 
3414  // Messages
3415  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3416  MessageCache::singleton()->updateMessageOverride( $title, null );
3417  }
3418 
3419  // Images
3420  if ( $title->getNamespace() == NS_FILE ) {
3422  new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
3423  );
3424  }
3425 
3426  // User talk pages
3427  if ( $title->getNamespace() == NS_USER_TALK ) {
3428  $user = User::newFromName( $title->getText(), false );
3429  if ( $user ) {
3430  $user->setNewtalk( false );
3431  }
3432  }
3433 
3434  // Image redirects
3435  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3436 
3437  // Purge cross-wiki cache entities referencing this page
3438  self::purgeInterwikiCheckKey( $title );
3439  }
3440 
3449  public static function onArticleEdit(
3450  Title $title,
3451  Revision $revision = null,
3452  $slotsChanged = null
3453  ) {
3454  // TODO: move this into a PageEventEmitter service
3455 
3456  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3457  // Invalidate caches of articles which include this page.
3458  // Only for the main slot, because only the main slot is transcluded.
3459  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3461  new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
3462  );
3463  }
3464 
3465  // Invalidate the caches of all pages which redirect here
3467  new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
3468  );
3469 
3470  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3471 
3472  // Purge CDN for this page only
3473  $title->purgeSquid();
3474  // Clear file cache for this page only
3476 
3477  // Purge ?action=info cache
3478  $revid = $revision ? $revision->getId() : null;
3479  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3480  InfoAction::invalidateCache( $title, $revid );
3481  } );
3482 
3483  // Purge cross-wiki cache entities referencing this page
3484  self::purgeInterwikiCheckKey( $title );
3485  }
3486 
3494  private static function purgeInterwikiCheckKey( Title $title ) {
3496 
3497  if ( !$wgEnableScaryTranscluding ) {
3498  return; // @todo: perhaps this wiki is only used as a *source* for content?
3499  }
3500 
3501  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3502  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3503  $cache->resetCheckKey(
3504  // Do not include the namespace since there can be multiple aliases to it
3505  // due to different namespace text definitions on different wikis. This only
3506  // means that some cache invalidations happen that are not strictly needed.
3507  $cache->makeGlobalKey(
3508  'interwiki-page',
3509  WikiMap::getCurrentWikiDomain()->getId(),
3510  $title->getDBkey()
3511  )
3512  );
3513  } );
3514  }
3515 
3522  public function getCategories() {
3523  $id = $this->getId();
3524  if ( $id == 0 ) {
3525  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3526  }
3527 
3528  $dbr = wfGetDB( DB_REPLICA );
3529  $res = $dbr->select( 'categorylinks',
3530  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3531  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3532  // as not being aliases, and NS_CATEGORY is numeric
3533  [ 'cl_from' => $id ],
3534  __METHOD__ );
3535 
3536  return TitleArray::newFromResult( $res );
3537  }
3538 
3545  public function getHiddenCategories() {
3546  $result = [];
3547  $id = $this->getId();
3548 
3549  if ( $id == 0 ) {
3550  return [];
3551  }
3552 
3553  $dbr = wfGetDB( DB_REPLICA );
3554  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3555  [ 'cl_to' ],
3556  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3557  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3558  __METHOD__ );
3559 
3560  if ( $res !== false ) {
3561  foreach ( $res as $row ) {
3562  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3563  }
3564  }
3565 
3566  return $result;
3567  }
3568 
3576  public function getAutoDeleteReason( &$hasHistory ) {
3577  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3578  }
3579 
3590  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3591  $id = $id ?: $this->getId();
3592  $type = MWNamespace::getCategoryLinkType( $this->getTitle()->getNamespace() );
3593 
3594  $addFields = [ 'cat_pages = cat_pages + 1' ];
3595  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3596  if ( $type !== 'page' ) {
3597  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3598  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3599  }
3600 
3601  $dbw = wfGetDB( DB_MASTER );
3602 
3603  if ( count( $added ) ) {
3604  $existingAdded = $dbw->selectFieldValues(
3605  'category',
3606  'cat_title',
3607  [ 'cat_title' => $added ],
3608  __METHOD__
3609  );
3610 
3611  // For category rows that already exist, do a plain
3612  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3613  // to avoid creating gaps in the cat_id sequence.
3614  if ( count( $existingAdded ) ) {
3615  $dbw->update(
3616  'category',
3617  $addFields,
3618  [ 'cat_title' => $existingAdded ],
3619  __METHOD__
3620  );
3621  }
3622 
3623  $missingAdded = array_diff( $added, $existingAdded );
3624  if ( count( $missingAdded ) ) {
3625  $insertRows = [];
3626  foreach ( $missingAdded as $cat ) {
3627  $insertRows[] = [
3628  'cat_title' => $cat,
3629  'cat_pages' => 1,
3630  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3631  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3632  ];
3633  }
3634  $dbw->upsert(
3635  'category',
3636  $insertRows,
3637  [ 'cat_title' ],
3638  $addFields,
3639  __METHOD__
3640  );
3641  }
3642  }
3643 
3644  if ( count( $deleted ) ) {
3645  $dbw->update(
3646  'category',
3647  $removeFields,
3648  [ 'cat_title' => $deleted ],
3649  __METHOD__
3650  );
3651  }
3652 
3653  foreach ( $added as $catName ) {
3654  $cat = Category::newFromName( $catName );
3655  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3656  }
3657 
3658  foreach ( $deleted as $catName ) {
3659  $cat = Category::newFromName( $catName );
3660  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3661  // Refresh counts on categories that should be empty now (after commit, T166757)
3662  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3663  $cat->refreshCountsIfEmpty();
3664  } );
3665  }
3666  }
3667 
3674  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3675  if ( wfReadOnly() ) {
3676  return;
3677  }
3678 
3679  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3680  [ $this, $this->mTitle, $parserOutput ]
3681  ) ) {
3682  return;
3683  }
3684 
3685  $config = RequestContext::getMain()->getConfig();
3686 
3687  $params = [
3688  'isOpportunistic' => true,
3689  'rootJobTimestamp' => $parserOutput->getCacheTime()
3690  ];
3691 
3692  if ( $this->mTitle->areRestrictionsCascading() ) {
3693  // If the page is cascade protecting, the links should really be up-to-date
3694  JobQueueGroup::singleton()->lazyPush(
3695  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3696  );
3697  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3698  // Assume the output contains "dynamic" time/random based magic words.
3699  // Only update pages that expired due to dynamic content and NOT due to edits
3700  // to referenced templates/files. When the cache expires due to dynamic content,
3701  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3702  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3703  // template/file edit already triggered recursive RefreshLinksJob jobs.
3704  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3705  // If a page is uncacheable, do not keep spamming a job for it.
3706  // Although it would be de-duplicated, it would still waste I/O.
3708  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3709  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3710  if ( $cache->add( $key, time(), $ttl ) ) {
3711  JobQueueGroup::singleton()->lazyPush(
3712  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3713  );
3714  }
3715  }
3716  }
3717  }
3718 
3728  public function getDeletionUpdates( $rev = null ) {
3729  if ( !$rev ) {
3730  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3731 
3732  try {
3733  $rev = $this->getRevisionRecord();
3734  } catch ( Exception $ex ) {
3735  // If we can't load the content, something is wrong. Perhaps that's why
3736  // the user is trying to delete the page, so let's not fail in that case.
3737  // Note that doDeleteArticleReal() will already have logged an issue with
3738  // loading the content.
3739  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3740  }
3741  }
3742 
3743  if ( !$rev ) {
3744  $slotContent = [];
3745  } elseif ( $rev instanceof Content ) {
3746  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3747 
3748  $slotContent = [ SlotRecord::MAIN => $rev ];
3749  } else {
3750  $slotContent = array_map( function ( SlotRecord $slot ) {
3751  return $slot->getContent( Revision::RAW );
3752  }, $rev->getSlots()->getSlots() );
3753  }
3754 
3755  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3756 
3757  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3758  // model here, not the content object!
3759  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3761  foreach ( $slotContent as $role => $content ) {
3762  $handler = $content->getContentHandler();
3763 
3764  $updates = $handler->getDeletionUpdates(
3765  $this->getTitle(),
3766  $role
3767  );
3768  $allUpdates = array_merge( $allUpdates, $updates );
3769 
3770  // TODO: remove B/C hack in 1.32!
3771  $legacyUpdates = $content->getDeletionUpdates( $this );
3772 
3773  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3774  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3775  return !( $update instanceof LinksDeletionUpdate );
3776  } );
3777 
3778  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3779  }
3780 
3781  Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3782 
3783  // TODO: hard deprecate old hook in 1.33
3784  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3785  return $allUpdates;
3786  }
3787 
3795  public function isLocal() {
3796  return true;
3797  }
3798 
3808  public function getWikiDisplayName() {
3809  global $wgSitename;
3810  return $wgSitename;
3811  }
3812 
3821  public function getSourceURL() {
3822  return $this->getTitle()->getCanonicalURL();
3823  }
3824 
3831  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3832 
3833  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3834  }
3835 
3836 }
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:806
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:689
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:284
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:128
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3458
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2069
setLastEdit(Revision $revision)
Set the latest revision.
Definition: WikiPage.php:764
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:3362
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4926
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article...
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1590
static getMainWANInstance()
Get the main WAN cache object.
int $wgCommentTableSchemaMigrationStage
Comment table schema migration stage.
string $mLinksUpdated
Definition: WikiPage.php:97
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
static newFromName( $name)
Factory function.
Definition: Category.php:126
static getRequestId()
Get the unique request ID.
Definition: WebRequest.php:275
getLatest()
Get the page_latest field.
Definition: WikiPage.php:700
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3808
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:475
getParserCache()
Definition: WikiPage.php:246
string $mTouched
Definition: WikiPage.php:92
$wgSitename
Name of the site.
$wgUseAutomaticEditSummaries
If user doesn&#39;t specify any edit summary when making a an edit, MediaWiki will try to automatically c...
getRevisionRecord()
Definition: Revision.php:645
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:933
clearNotification(&$title, $oldid=0)
Clear the user&#39;s notification timestamp for the given title.
Definition: User.php:3998
int $mId
Definition: WikiPage.php:67
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:785
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
static newFromTitle( $title)
Factory function.
Definition: Category.php:146
getDBLoadBalancer()
Definition: WikiPage.php:253
int $wgMultiContentRevisionSchemaMigrationStage
RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables)...
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1995
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:72
updateCategoryCounts(array $added, array $deleted, $id=0)
Update all the appropriate counts in the category table, given that we&#39;ve added the categories $added...
Definition: WikiPage.php:3590
getTimestamp()
Definition: Revision.php:1012
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3856
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2461
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1031
Title $mTitle
Definition: WikiPage.php:51
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage.
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3494
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1276
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1102
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
const EDIT_INTERNAL
Definition: Defines.php:159
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:297
clear()
Clear the object.
Definition: WikiPage.php:292
Handles purging appropriate CDN URLs given a title (or titles)
$wgEnableScaryTranscluding
Enable interwiki transcluding.
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3674
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3821
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1318
$source
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:914
static newFromPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given page ID...
Definition: Revision.php:158
static getCurrentWikiDomain()
Definition: WikiMap.php:276
getOtherPage()
Get the other title for this page, if this is a subject page get the talk page, if it is a subject pa...
Definition: Title.php:1560
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2651
static getLocalClusterInstance()
Get the main cluster-local cache object.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
Value object representing a modification of revision slots.
getContributors()
Get a list of users who have edited this article, not including the user who made the most recent rev...
Definition: WikiPage.php:1140
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
newDerivedDataUpdater()
Definition: WikiPage.php:1674
const EDIT_MINOR
Definition: Defines.php:154
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
const EDIT_UPDATE
Definition: Defines.php:153
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:475
getTouched()
Get the page_touched field.
Definition: WikiPage.php:678
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
setTimestamp( $ts)
Set the page timestamp (use only to avoid DB queries)
Definition: WikiPage.php:831
This document provides an overview of the usage of PageUpdater and DerivedPageDataUpdater
Definition: pageupdater.txt:3
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition: WikiPage.php:87
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1613
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
const DB_MASTER
Definition: defines.php:26
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:303
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
loadLastEdit()
Loads everything except the text This isn&#39;t necessary for all uses, so it&#39;s only done if needed...
Definition: WikiPage.php:724
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:102
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3830
this hook is for auditing only RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition: hooks.txt:989
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2485
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object&#39;s namespace and title using a sanitized title string...
Definition: WikiPage.php:445
getActionOverrides()
Definition: WikiPage.php:263
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:620
Class DeletePageJob.
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
getRevisionStore()
Definition: WikiPage.php:225
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1993
if( $line===false) $args
Definition: cdb.php:64
getContentModel()
Returns the page&#39;s content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:638
static onArticleEdit(Title $title, Revision $revision=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3449
static newCanonical( $context=null, $userLang=null)
Creates a "canonical" ParserOptions object.
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1442
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:47
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:667
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility...
static get(Title $title)
Create a new BacklinkCache or reuse any existing one.
commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser, $tags=null)
Backend implementation of doRollback(), please refer there for parameter and return value documentati...
Definition: WikiPage.php:3121
$mIsRedirect
Definition: WikiPage.php:57
doDeleteArticleReal( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $deleter=null, $tags=[], $logsubtype='delete', $immediate=false)
Back-end article deletion Deletes the article with database consistency, writes logs, purges caches.
Definition: WikiPage.php:2615
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
getRevision()
Get the latest revision.
Definition: WikiPage.php:773
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
const PRC_PATROLLED
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:780
getRevisionRenderer()
Definition: WikiPage.php:232
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
newPageUpdater(User $user, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
Definition: WikiPage.php:1778
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:70
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:129
wfReadOnly()
Check whether the wiki is in read-only mode.
wfIncrStats( $key, $count=1)
Increment a statistics counter.
static newMigration()
Static constructor.
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
deleteTitleProtection()
Remove any title protection due to page existing.
Definition: Title.php:3017
static getMain()
Get the RequestContext object associated with the main request.
isBatchedDelete( $safetyMargin=0)
Determines if deletion of this page would be batched (executed over time by the job queue) or not (co...
Definition: WikiPage.php:2553
const FOR_PUBLIC
Definition: Revision.php:56
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
const EDIT_FORCE_BOT
Definition: Defines.php:156
static clearFileCache(Title $title)
Clear the file caches for a page for all actions.
doDeleteArticle( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $user=null, $immediate=false)
Same as doDeleteArticleReal(), but returns a simple boolean.
Definition: WikiPage.php:2582
doDeleteUpdates( $id, Content $content=null, Revision $revision=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:2995
Revision $mLastRevision
Definition: WikiPage.php:82
Class to invalidate the HTML cache of all the pages linking to a given title.
getDBkey()
Get the main part with underscores.
Definition: Title.php:951
updateIfNewerOn( $dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record...
Definition: WikiPage.php:1477
$wgAjaxEditStash
Have clients send edits to be prepared when filling in edit summaries.
static factory(array $deltas)
static newKnownCurrent(IDatabase $db, $pageIdOrTitle, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: Revision.php:1345
getSlotRoleRegistry()
Definition: WikiPage.php:239
const MIGRATION_OLD
Definition: Defines.php:315
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:286
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1091
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1574
const NS_MEDIA
Definition: Defines.php:52
$res
Definition: database.txt:21
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:61
getContentHandler()
Returns the content handler appropriate for this revision&#39;s content model.
Definition: Revision.php:1005
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
const GAID_FOR_UPDATE
Used to be GAID_FOR_UPDATE define.
Definition: Title.php:54
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:116
$wgRCWatchCategoryMembership
Treat category membership changes as a RecentChange.
PreparedEdit $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:62
getCacheTime()
Definition: CacheTime.php:60
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:711
$cache
Definition: mcc.php:33
$params
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3545
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1253
getTitle()
Get the title object of the article.
Definition: WikiPage.php:284
const NS_CATEGORY
Definition: Defines.php:78
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3886
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1995
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:57
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
static newFromResult( $res)
Definition: TitleArray.php:40
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:935
hasViewableContent()
Check if this page is something we&#39;re going to be showing some sort of sensible content for...
Definition: WikiPage.php:611
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object...
Definition: Revision.php:527
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:975
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
const NS_FILE
Definition: Defines.php:70
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1779
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:861
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:546
const RAW
Definition: Revision.php:58
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:651
Special handling for file pages.
const NS_MEDIAWIKI
Definition: Defines.php:72
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:276
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1276
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2110
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3576
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2396
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:1534
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
$wgPageCreationLog
Maintain a log of page creations at Special:Log/create?
getStubThreshold()
Thumb size preferred by the user.
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1934
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:931
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:545
static queueRecursiveJobsForTable(Title $title, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:166
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
getComment( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:900
getUser( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:844
static newDynamic(Title $title, array $params)
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:608
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:675
wfRandom()
Get a random decimal value in the domain of [0, 1), in a way not likely to give duplicate values for ...
getUserText( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:882
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
static getStore()
getCategories()
Returns a list of categories this page is a member of.
Definition: WikiPage.php:3522
static convertSelectType( $type)
Convert &#39;fromdb&#39;, &#39;fromdbmaster&#39; and &#39;forupdate&#39; to READ_* constants.
Definition: WikiPage.php:208
hasDynamicContent()
Check whether the cache TTL was lowered due to dynamic content.
doEditUpdates(Revision $revision, User $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2041
getId()
Get the user&#39;s ID.
Definition: User.php:2460
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4701
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:2135
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3795
const EDIT_NEW
Definition: Defines.php:152
getTimestamp()
Definition: WikiPage.php:817
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:459
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:1722
static hasDifferencesOutsideMainSlot(Revision $a, Revision $b)
Helper method for checking whether two revisions have differences that go beyond the main slot...
Definition: WikiPage.php:1515
Controller-like object for creating and updating pages by creating new revisions. ...
Definition: PageUpdater.php:72
Overloads the relevant methods of the real ResultsWrapper so it doesn&#39;t go anywhere near an actual da...
getDeletionUpdates( $rev=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3728
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3395
if(count( $args)< 1) $job
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
supportsSections()
Returns true if this page&#39;s content model supports sections.
Definition: WikiPage.php:1556
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:3075
static newPrioritized(Title $title, array $params)
$page->newPageUpdater($user) $updater
Definition: pageupdater.txt:63
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1659
$revQuery
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:412
$mDataLoaded
Definition: WikiPage.php:56
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1190
static invalidateModuleCache(Title $title, Revision $old=null, Revision $new=null, $wikiId)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition: hooks.txt:2625
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:984
formatExpiry( $expiry)
Definition: WikiPage.php:2439
MediaWiki Logger LoggerFactory implements a PSR [0] compatible message logging system Named Psr Log LoggerInterface instances can be obtained from the MediaWiki Logger LoggerFactory::getInstance() static method. MediaWiki\Logger\LoggerFactory expects a class implementing the MediaWiki\Logger\Spi interface to act as a factory for new Psr\Log\LoggerInterface instances. The "Spi" in MediaWiki\Logger\Spi stands for "service provider interface". An SPI is an API intended to be implemented or extended by a third party. This software design pattern is intended to enable framework extension and replaceable components. It is specifically used in the MediaWiki\Logger\LoggerFactory service to allow alternate PSR-3 logging implementations to be easily integrated with MediaWiki. The service provider interface allows the backend logging library to be implemented in multiple ways. The $wgMWLoggerDefaultSpi global provides the classname of the default MediaWiki\Logger\Spi implementation to be loaded at runtime. This can either be the name of a class implementing the MediaWiki\Logger\Spi with a zero argument const ructor or a callable that will return an MediaWiki\Logger\Spi instance. Alternately the MediaWiki\Logger\LoggerFactory MediaWiki Logger LoggerFactory
Definition: logger.txt:5
static singleton( $domain=false)
insertRedirectEntry(Title $rt, $oldLatest=null)
Insert or update the redirect table entry for this page to indicate it redirects to $rt...
Definition: WikiPage.php:1056
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1212
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1363
Page revision base class.
isSafeToCache()
Test whether these options are safe to cache.
static flattenRestrictions( $limit)
Take an array of page restrictions and flatten it to a string suitable for insertion into the page_re...
Definition: WikiPage.php:2526
const DB_REPLICA
Definition: defines.php:25
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2967
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:1859
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:585
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:120
static selectFields()
Return the list of revision fields that should be selected to create a new page.
Definition: WikiPage.php:334
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:323
const PRC_AUTOPATROLLED
$content
Definition: pageupdater.txt:72
const NS_USER_TALK
Definition: Defines.php:67
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:1966
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Title $mRedirectTarget
Definition: WikiPage.php:77
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:108
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page&#39;s history.
Definition: Revision.php:1216
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object...
Definition: WikiPage.php:373
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:2816
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:196
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:520
Special handling for category pages.
static singleton()
Get the signleton instance of this class.
purgeSquid()
Purge all applicable CDN URLs.
Definition: Title.php:4021
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1486
static getCategoryLinkType( $index)
Returns the link type to be used for categories.
doUpdateRestrictions(array $limit, array $expiry, &$cascade, $reason, User $user, $tags=null)
Update the article&#39;s restriction field, and leave a log entry.
Definition: WikiPage.php:2140
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
const SUPPRESSED_ALL
Definition: Revision.php:53
getCreator( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:863
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2503