MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
37 
44 class WikiPage implements Page, IDBAccessObject {
45  // Constants for $mDataLoadedFrom and related
46 
50  public $mTitle = null;
51 
55  public $mDataLoaded = false; // !< Boolean
56  public $mIsRedirect = false; // !< Boolean
57  public $mLatest = false; // !< Integer (false means "not loaded")
61  public $mPreparedEdit = false;
62 
66  protected $mId = null;
67 
71  protected $mDataLoadedFrom = self::READ_NONE;
72 
76  protected $mRedirectTarget = null;
77 
81  protected $mLastRevision = null;
82 
86  protected $mTimestamp = '';
87 
91  protected $mTouched = '19700101000000';
92 
96  protected $mLinksUpdated = '19700101000000';
97 
101  private $derivedDataUpdater = null;
102 
107  public function __construct( Title $title ) {
108  $this->mTitle = $title;
109  }
110 
115  public function __clone() {
116  $this->mTitle = clone $this->mTitle;
117  }
118 
127  public static function factory( Title $title ) {
128  $ns = $title->getNamespace();
129 
130  if ( $ns == NS_MEDIA ) {
131  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
132  } elseif ( $ns < 0 ) {
133  throw new MWException( "Invalid or virtual namespace $ns given." );
134  }
135 
136  $page = null;
137  if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
138  return $page;
139  }
140 
141  switch ( $ns ) {
142  case NS_FILE:
143  $page = new WikiFilePage( $title );
144  break;
145  case NS_CATEGORY:
146  $page = new WikiCategoryPage( $title );
147  break;
148  default:
149  $page = new WikiPage( $title );
150  }
151 
152  return $page;
153  }
154 
165  public static function newFromID( $id, $from = 'fromdb' ) {
166  // page ids are never 0 or negative, see T63166
167  if ( $id < 1 ) {
168  return null;
169  }
170 
171  $from = self::convertSelectType( $from );
172  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
173  $pageQuery = self::getQueryInfo();
174  $row = $db->selectRow(
175  $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
176  [], $pageQuery['joins']
177  );
178  if ( !$row ) {
179  return null;
180  }
181  return self::newFromRow( $row, $from );
182  }
183 
195  public static function newFromRow( $row, $from = 'fromdb' ) {
196  $page = self::factory( Title::newFromRow( $row ) );
197  $page->loadFromRow( $row, $from );
198  return $page;
199  }
200 
207  protected static function convertSelectType( $type ) {
208  switch ( $type ) {
209  case 'fromdb':
210  return self::READ_NORMAL;
211  case 'fromdbmaster':
212  return self::READ_LATEST;
213  case 'forupdate':
214  return self::READ_LOCKING;
215  default:
216  // It may already be an integer or whatever else
217  return $type;
218  }
219  }
220 
224  private function getRevisionStore() {
225  return MediaWikiServices::getInstance()->getRevisionStore();
226  }
227 
231  private function getRevisionRenderer() {
232  return MediaWikiServices::getInstance()->getRevisionRenderer();
233  }
234 
238  private function getParserCache() {
239  return MediaWikiServices::getInstance()->getParserCache();
240  }
241 
245  private function getDBLoadBalancer() {
246  return MediaWikiServices::getInstance()->getDBLoadBalancer();
247  }
248 
255  public function getActionOverrides() {
256  return $this->getContentHandler()->getActionOverrides();
257  }
258 
268  public function getContentHandler() {
270  }
271 
276  public function getTitle() {
277  return $this->mTitle;
278  }
279 
284  public function clear() {
285  $this->mDataLoaded = false;
286  $this->mDataLoadedFrom = self::READ_NONE;
287 
288  $this->clearCacheFields();
289  }
290 
295  protected function clearCacheFields() {
296  $this->mId = null;
297  $this->mRedirectTarget = null; // Title object if set
298  $this->mLastRevision = null; // Latest revision
299  $this->mTouched = '19700101000000';
300  $this->mLinksUpdated = '19700101000000';
301  $this->mTimestamp = '';
302  $this->mIsRedirect = false;
303  $this->mLatest = false;
304  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
305  // checks the requested rev ID and content against the cached one. For most
306  // content types, the output should not change during the lifetime of this cache.
307  // Clearing it can cause extra parses on edit for no reason.
308  }
309 
315  public function clearPreparedEdit() {
316  $this->mPreparedEdit = false;
317  }
318 
326  public static function selectFields() {
328 
329  wfDeprecated( __METHOD__, '1.31' );
330 
331  $fields = [
332  'page_id',
333  'page_namespace',
334  'page_title',
335  'page_restrictions',
336  'page_is_redirect',
337  'page_is_new',
338  'page_random',
339  'page_touched',
340  'page_links_updated',
341  'page_latest',
342  'page_len',
343  ];
344 
345  if ( $wgContentHandlerUseDB ) {
346  $fields[] = 'page_content_model';
347  }
348 
349  if ( $wgPageLanguageUseDB ) {
350  $fields[] = 'page_lang';
351  }
352 
353  return $fields;
354  }
355 
365  public static function getQueryInfo() {
367 
368  $ret = [
369  'tables' => [ 'page' ],
370  'fields' => [
371  'page_id',
372  'page_namespace',
373  'page_title',
374  'page_restrictions',
375  'page_is_redirect',
376  'page_is_new',
377  'page_random',
378  'page_touched',
379  'page_links_updated',
380  'page_latest',
381  'page_len',
382  ],
383  'joins' => [],
384  ];
385 
386  if ( $wgContentHandlerUseDB ) {
387  $ret['fields'][] = 'page_content_model';
388  }
389 
390  if ( $wgPageLanguageUseDB ) {
391  $ret['fields'][] = 'page_lang';
392  }
393 
394  return $ret;
395  }
396 
404  protected function pageData( $dbr, $conditions, $options = [] ) {
405  $pageQuery = self::getQueryInfo();
406 
407  // Avoid PHP 7.1 warning of passing $this by reference
408  $wikiPage = $this;
409 
410  Hooks::run( 'ArticlePageDataBefore', [
411  &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
412  ] );
413 
414  $row = $dbr->selectRow(
415  $pageQuery['tables'],
416  $pageQuery['fields'],
417  $conditions,
418  __METHOD__,
419  $options,
420  $pageQuery['joins']
421  );
422 
423  Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
424 
425  return $row;
426  }
427 
437  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
438  return $this->pageData( $dbr, [
439  'page_namespace' => $title->getNamespace(),
440  'page_title' => $title->getDBkey() ], $options );
441  }
442 
451  public function pageDataFromId( $dbr, $id, $options = [] ) {
452  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
453  }
454 
467  public function loadPageData( $from = 'fromdb' ) {
468  $from = self::convertSelectType( $from );
469  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
470  // We already have the data from the correct location, no need to load it twice.
471  return;
472  }
473 
474  if ( is_int( $from ) ) {
475  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
476  $loadBalancer = $this->getDBLoadBalancer();
477  $db = $loadBalancer->getConnection( $index );
478  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
479 
480  if ( !$data
481  && $index == DB_REPLICA
482  && $loadBalancer->getServerCount() > 1
483  && $loadBalancer->hasOrMadeRecentMasterChanges()
484  ) {
485  $from = self::READ_LATEST;
486  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
487  $db = $loadBalancer->getConnection( $index );
488  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
489  }
490  } else {
491  // No idea from where the caller got this data, assume replica DB.
492  $data = $from;
493  $from = self::READ_NORMAL;
494  }
495 
496  $this->loadFromRow( $data, $from );
497  }
498 
512  public function wasLoadedFrom( $from ) {
513  $from = self::convertSelectType( $from );
514 
515  if ( !is_int( $from ) ) {
516  // No idea from where the caller got this data, assume replica DB.
517  $from = self::READ_NORMAL;
518  }
519 
520  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
521  return true;
522  }
523 
524  return false;
525  }
526 
538  public function loadFromRow( $data, $from ) {
539  $lc = MediaWikiServices::getInstance()->getLinkCache();
540  $lc->clearLink( $this->mTitle );
541 
542  if ( $data ) {
543  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
544 
545  $this->mTitle->loadFromRow( $data );
546 
547  // Old-fashioned restrictions
548  $this->mTitle->loadRestrictions( $data->page_restrictions );
549 
550  $this->mId = intval( $data->page_id );
551  $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
552  $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
553  $this->mIsRedirect = intval( $data->page_is_redirect );
554  $this->mLatest = intval( $data->page_latest );
555  // T39225: $latest may no longer match the cached latest Revision object.
556  // Double-check the ID of any cached latest Revision object for consistency.
557  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
558  $this->mLastRevision = null;
559  $this->mTimestamp = '';
560  }
561  } else {
562  $lc->addBadLinkObj( $this->mTitle );
563 
564  $this->mTitle->loadFromRow( false );
565 
566  $this->clearCacheFields();
567 
568  $this->mId = 0;
569  }
570 
571  $this->mDataLoaded = true;
572  $this->mDataLoadedFrom = self::convertSelectType( $from );
573  }
574 
578  public function getId() {
579  if ( !$this->mDataLoaded ) {
580  $this->loadPageData();
581  }
582  return $this->mId;
583  }
584 
588  public function exists() {
589  if ( !$this->mDataLoaded ) {
590  $this->loadPageData();
591  }
592  return $this->mId > 0;
593  }
594 
603  public function hasViewableContent() {
604  return $this->mTitle->isKnown();
605  }
606 
612  public function isRedirect() {
613  if ( !$this->mDataLoaded ) {
614  $this->loadPageData();
615  }
616 
617  return (bool)$this->mIsRedirect;
618  }
619 
630  public function getContentModel() {
631  if ( $this->exists() ) {
633 
634  return $cache->getWithSetCallback(
635  $cache->makeKey( 'page-content-model', $this->getLatest() ),
636  $cache::TTL_MONTH,
637  function () {
638  $rev = $this->getRevision();
639  if ( $rev ) {
640  // Look at the revision's actual content model
641  return $rev->getContentModel();
642  } else {
643  $title = $this->mTitle->getPrefixedDBkey();
644  wfWarn( "Page $title exists but has no (visible) revisions!" );
645  return $this->mTitle->getContentModel();
646  }
647  }
648  );
649  }
650 
651  // use the default model for this page
652  return $this->mTitle->getContentModel();
653  }
654 
659  public function checkTouched() {
660  if ( !$this->mDataLoaded ) {
661  $this->loadPageData();
662  }
663  return ( $this->mId && !$this->mIsRedirect );
664  }
665 
670  public function getTouched() {
671  if ( !$this->mDataLoaded ) {
672  $this->loadPageData();
673  }
674  return $this->mTouched;
675  }
676 
681  public function getLinksTimestamp() {
682  if ( !$this->mDataLoaded ) {
683  $this->loadPageData();
684  }
685  return $this->mLinksUpdated;
686  }
687 
692  public function getLatest() {
693  if ( !$this->mDataLoaded ) {
694  $this->loadPageData();
695  }
696  return (int)$this->mLatest;
697  }
698 
703  public function getOldestRevision() {
704  // Try using the replica DB first, then try the master
705  $rev = $this->mTitle->getFirstRevision();
706  if ( !$rev ) {
707  $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE );
708  }
709  return $rev;
710  }
711 
716  protected function loadLastEdit() {
717  if ( $this->mLastRevision !== null ) {
718  return; // already loaded
719  }
720 
721  $latest = $this->getLatest();
722  if ( !$latest ) {
723  return; // page doesn't exist or is missing page_latest info
724  }
725 
726  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
727  // T39225: if session S1 loads the page row FOR UPDATE, the result always
728  // includes the latest changes committed. This is true even within REPEATABLE-READ
729  // transactions, where S1 normally only sees changes committed before the first S1
730  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
731  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
732  // happened after the first S1 SELECT.
733  // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
734  $flags = Revision::READ_LOCKING;
735  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
736  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
737  // Bug T93976: if page_latest was loaded from the master, fetch the
738  // revision from there as well, as it may not exist yet on a replica DB.
739  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
740  $flags = Revision::READ_LATEST;
741  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
742  } else {
743  $dbr = wfGetDB( DB_REPLICA );
744  $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
745  }
746 
747  if ( $revision ) { // sanity
748  $this->setLastEdit( $revision );
749  }
750  }
751 
756  protected function setLastEdit( Revision $revision ) {
757  $this->mLastRevision = $revision;
758  $this->mTimestamp = $revision->getTimestamp();
759  }
760 
765  public function getRevision() {
766  $this->loadLastEdit();
767  if ( $this->mLastRevision ) {
768  return $this->mLastRevision;
769  }
770  return null;
771  }
772 
777  public function getRevisionRecord() {
778  $this->loadLastEdit();
779  if ( $this->mLastRevision ) {
780  return $this->mLastRevision->getRevisionRecord();
781  }
782  return null;
783  }
784 
798  public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
799  $this->loadLastEdit();
800  if ( $this->mLastRevision ) {
801  return $this->mLastRevision->getContent( $audience, $user );
802  }
803  return null;
804  }
805 
809  public function getTimestamp() {
810  // Check if the field has been filled by WikiPage::setTimestamp()
811  if ( !$this->mTimestamp ) {
812  $this->loadLastEdit();
813  }
814 
815  return wfTimestamp( TS_MW, $this->mTimestamp );
816  }
817 
823  public function setTimestamp( $ts ) {
824  $this->mTimestamp = wfTimestamp( TS_MW, $ts );
825  }
826 
836  public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
837  $this->loadLastEdit();
838  if ( $this->mLastRevision ) {
839  return $this->mLastRevision->getUser( $audience, $user );
840  } else {
841  return -1;
842  }
843  }
844 
855  public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
856  $revision = $this->getOldestRevision();
857  if ( $revision ) {
858  $userName = $revision->getUserText( $audience, $user );
859  return User::newFromName( $userName, false );
860  } else {
861  return null;
862  }
863  }
864 
874  public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
875  $this->loadLastEdit();
876  if ( $this->mLastRevision ) {
877  return $this->mLastRevision->getUserText( $audience, $user );
878  } else {
879  return '';
880  }
881  }
882 
892  public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
893  $this->loadLastEdit();
894  if ( $this->mLastRevision ) {
895  return $this->mLastRevision->getComment( $audience, $user );
896  } else {
897  return '';
898  }
899  }
900 
906  public function getMinorEdit() {
907  $this->loadLastEdit();
908  if ( $this->mLastRevision ) {
909  return $this->mLastRevision->isMinor();
910  } else {
911  return false;
912  }
913  }
914 
923  public function isCountable( $editInfo = false ) {
924  global $wgArticleCountMethod;
925 
926  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
927 
928  if ( !$this->mTitle->isContentPage() ) {
929  return false;
930  }
931 
932  if ( $editInfo ) {
933  // NOTE: only the main slot can make a page a redirect
934  $content = $editInfo->pstContent;
935  } else {
936  $content = $this->getContent();
937  }
938 
939  if ( !$content || $content->isRedirect() ) {
940  return false;
941  }
942 
943  $hasLinks = null;
944 
945  if ( $wgArticleCountMethod === 'link' ) {
946  // nasty special case to avoid re-parsing to detect links
947 
948  if ( $editInfo ) {
949  // ParserOutput::getLinks() is a 2D array of page links, so
950  // to be really correct we would need to recurse in the array
951  // but the main array should only have items in it if there are
952  // links.
953  $hasLinks = (bool)count( $editInfo->output->getLinks() );
954  } else {
955  // NOTE: keep in sync with revisionRenderer::getLinkCount
956  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
957  [ 'pl_from' => $this->getId() ], __METHOD__ );
958  }
959  }
960 
961  return $content->isCountable( $hasLinks );
962  }
963 
971  public function getRedirectTarget() {
972  if ( !$this->mTitle->isRedirect() ) {
973  return null;
974  }
975 
976  if ( $this->mRedirectTarget !== null ) {
977  return $this->mRedirectTarget;
978  }
979 
980  // Query the redirect table
981  $dbr = wfGetDB( DB_REPLICA );
982  $row = $dbr->selectRow( 'redirect',
983  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
984  [ 'rd_from' => $this->getId() ],
985  __METHOD__
986  );
987 
988  // rd_fragment and rd_interwiki were added later, populate them if empty
989  if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
990  // (T203942) We can't redirect to Media namespace because it's virtual.
991  // We don't want to modify Title objects farther down the
992  // line. So, let's fix this here by changing to File namespace.
993  if ( $row->rd_namespace == NS_MEDIA ) {
994  $namespace = NS_FILE;
995  } else {
996  $namespace = $row->rd_namespace;
997  }
998  $this->mRedirectTarget = Title::makeTitle(
999  $namespace, $row->rd_title,
1000  $row->rd_fragment, $row->rd_interwiki
1001  );
1002  return $this->mRedirectTarget;
1003  }
1004 
1005  // This page doesn't have an entry in the redirect table
1006  $this->mRedirectTarget = $this->insertRedirect();
1007  return $this->mRedirectTarget;
1008  }
1009 
1018  public function insertRedirect() {
1019  $content = $this->getContent();
1020  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1021  if ( !$retval ) {
1022  return null;
1023  }
1024 
1025  // Update the DB post-send if the page has not cached since now
1026  $latest = $this->getLatest();
1028  function () use ( $retval, $latest ) {
1029  $this->insertRedirectEntry( $retval, $latest );
1030  },
1032  wfGetDB( DB_MASTER )
1033  );
1034 
1035  return $retval;
1036  }
1037 
1043  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1044  $dbw = wfGetDB( DB_MASTER );
1045  $dbw->startAtomic( __METHOD__ );
1046 
1047  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1048  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1049  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1050  $dbw->upsert(
1051  'redirect',
1052  [
1053  'rd_from' => $this->getId(),
1054  'rd_namespace' => $rt->getNamespace(),
1055  'rd_title' => $rt->getDBkey(),
1056  'rd_fragment' => $truncatedFragment,
1057  'rd_interwiki' => $rt->getInterwiki(),
1058  ],
1059  [ 'rd_from' ],
1060  [
1061  'rd_namespace' => $rt->getNamespace(),
1062  'rd_title' => $rt->getDBkey(),
1063  'rd_fragment' => $truncatedFragment,
1064  'rd_interwiki' => $rt->getInterwiki(),
1065  ],
1066  __METHOD__
1067  );
1068  }
1069 
1070  $dbw->endAtomic( __METHOD__ );
1071  }
1072 
1078  public function followRedirect() {
1079  return $this->getRedirectURL( $this->getRedirectTarget() );
1080  }
1081 
1089  public function getRedirectURL( $rt ) {
1090  if ( !$rt ) {
1091  return false;
1092  }
1093 
1094  if ( $rt->isExternal() ) {
1095  if ( $rt->isLocal() ) {
1096  // Offsite wikis need an HTTP redirect.
1097  // This can be hard to reverse and may produce loops,
1098  // so they may be disabled in the site configuration.
1099  $source = $this->mTitle->getFullURL( 'redirect=no' );
1100  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1101  } else {
1102  // External pages without "local" bit set are not valid
1103  // redirect targets
1104  return false;
1105  }
1106  }
1107 
1108  if ( $rt->isSpecialPage() ) {
1109  // Gotta handle redirects to special pages differently:
1110  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1111  // Some pages are not valid targets.
1112  if ( $rt->isValidRedirectTarget() ) {
1113  return $rt->getFullURL();
1114  } else {
1115  return false;
1116  }
1117  }
1118 
1119  return $rt;
1120  }
1121 
1127  public function getContributors() {
1128  // @todo: This is expensive; cache this info somewhere.
1129 
1130  $dbr = wfGetDB( DB_REPLICA );
1131 
1132  $actorMigration = ActorMigration::newMigration();
1133  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1134 
1135  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1136 
1137  $fields = [
1138  'user_id' => $actorQuery['fields']['rev_user'],
1139  'user_name' => $actorQuery['fields']['rev_user_text'],
1140  'actor_id' => $actorQuery['fields']['rev_actor'],
1141  'user_real_name' => 'MIN(user_real_name)',
1142  'timestamp' => 'MAX(rev_timestamp)',
1143  ];
1144 
1145  $conds = [ 'rev_page' => $this->getId() ];
1146 
1147  // The user who made the top revision gets credited as "this page was last edited by
1148  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1149  $user = $this->getUser()
1150  ? User::newFromId( $this->getUser() )
1151  : User::newFromName( $this->getUserText(), false );
1152  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1153 
1154  // Username hidden?
1155  $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1156 
1157  $jconds = [
1158  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1159  ] + $actorQuery['joins'];
1160 
1161  $options = [
1162  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1163  'ORDER BY' => 'timestamp DESC',
1164  ];
1165 
1166  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1167  return new UserArrayFromResult( $res );
1168  }
1169 
1177  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1178  return $parserOptions->getStubThreshold() == 0
1179  && $this->exists()
1180  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1181  && $this->getContentHandler()->isParserCacheSupported();
1182  }
1183 
1199  public function getParserOutput(
1200  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1201  ) {
1202  $useParserCache =
1203  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1204 
1205  if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1206  throw new InvalidArgumentException(
1207  'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1208  );
1209  }
1210 
1211  wfDebug( __METHOD__ .
1212  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1213  if ( $parserOptions->getStubThreshold() ) {
1214  wfIncrStats( 'pcache.miss.stub' );
1215  }
1216 
1217  if ( $useParserCache ) {
1218  $parserOutput = $this->getParserCache()
1219  ->get( $this, $parserOptions );
1220  if ( $parserOutput !== false ) {
1221  return $parserOutput;
1222  }
1223  }
1224 
1225  if ( $oldid === null || $oldid === 0 ) {
1226  $oldid = $this->getLatest();
1227  }
1228 
1229  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1230  $pool->execute();
1231 
1232  return $pool->getParserOutput();
1233  }
1234 
1240  public function doViewUpdates( User $user, $oldid = 0 ) {
1241  if ( wfReadOnly() ) {
1242  return;
1243  }
1244 
1245  // Update newtalk / watchlist notification status;
1246  // Avoid outage if the master is not reachable by using a deferred updated
1248  function () use ( $user, $oldid ) {
1249  Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1250 
1251  $user->clearNotification( $this->mTitle, $oldid );
1252  },
1254  );
1255  }
1256 
1263  public function doPurge() {
1264  // Avoid PHP 7.1 warning of passing $this by reference
1265  $wikiPage = $this;
1266 
1267  if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1268  return false;
1269  }
1270 
1271  $this->mTitle->invalidateCache();
1272 
1273  // Clear file cache
1275  // Send purge after above page_touched update was committed
1277  new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1279  );
1280 
1281  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1282  $messageCache = MessageCache::singleton();
1283  $messageCache->updateMessageOverride( $this->mTitle, $this->getContent() );
1284  }
1285 
1286  return true;
1287  }
1288 
1305  public function insertOn( $dbw, $pageId = null ) {
1306  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1307  $dbw->insert(
1308  'page',
1309  [
1310  'page_namespace' => $this->mTitle->getNamespace(),
1311  'page_title' => $this->mTitle->getDBkey(),
1312  'page_restrictions' => '',
1313  'page_is_redirect' => 0, // Will set this shortly...
1314  'page_is_new' => 1,
1315  'page_random' => wfRandom(),
1316  'page_touched' => $dbw->timestamp(),
1317  'page_latest' => 0, // Fill this in shortly...
1318  'page_len' => 0, // Fill this in shortly...
1319  ] + $pageIdForInsert,
1320  __METHOD__,
1321  'IGNORE'
1322  );
1323 
1324  if ( $dbw->affectedRows() > 0 ) {
1325  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1326  $this->mId = $newid;
1327  $this->mTitle->resetArticleID( $newid );
1328 
1329  return $newid;
1330  } else {
1331  return false; // nothing changed
1332  }
1333  }
1334 
1350  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1351  $lastRevIsRedirect = null
1352  ) {
1353  global $wgContentHandlerUseDB;
1354 
1355  // TODO: move into PageUpdater or PageStore
1356  // NOTE: when doing that, make sure cached fields get reset in doEditContent,
1357  // and in the compat stub!
1358 
1359  // Assertion to try to catch T92046
1360  if ( (int)$revision->getId() === 0 ) {
1361  throw new InvalidArgumentException(
1362  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1363  );
1364  }
1365 
1366  $content = $revision->getContent();
1367  $len = $content ? $content->getSize() : 0;
1368  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1369 
1370  $conditions = [ 'page_id' => $this->getId() ];
1371 
1372  if ( !is_null( $lastRevision ) ) {
1373  // An extra check against threads stepping on each other
1374  $conditions['page_latest'] = $lastRevision;
1375  }
1376 
1377  $revId = $revision->getId();
1378  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1379 
1380  $row = [ /* SET */
1381  'page_latest' => $revId,
1382  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1383  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1384  'page_is_redirect' => $rt !== null ? 1 : 0,
1385  'page_len' => $len,
1386  ];
1387 
1388  if ( $wgContentHandlerUseDB ) {
1389  $row['page_content_model'] = $revision->getContentModel();
1390  }
1391 
1392  $dbw->update( 'page',
1393  $row,
1394  $conditions,
1395  __METHOD__ );
1396 
1397  $result = $dbw->affectedRows() > 0;
1398  if ( $result ) {
1399  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1400  $this->setLastEdit( $revision );
1401  $this->mLatest = $revision->getId();
1402  $this->mIsRedirect = (bool)$rt;
1403  // Update the LinkCache.
1404  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1405  $linkCache->addGoodLinkObj(
1406  $this->getId(),
1407  $this->mTitle,
1408  $len,
1409  $this->mIsRedirect,
1410  $this->mLatest,
1411  $revision->getContentModel()
1412  );
1413  }
1414 
1415  return $result;
1416  }
1417 
1429  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1430  // Always update redirects (target link might have changed)
1431  // Update/Insert if we don't know if the last revision was a redirect or not
1432  // Delete if changing from redirect to non-redirect
1433  $isRedirect = !is_null( $redirectTitle );
1434 
1435  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1436  return true;
1437  }
1438 
1439  if ( $isRedirect ) {
1440  $this->insertRedirectEntry( $redirectTitle );
1441  } else {
1442  // This is not a redirect, remove row from redirect table
1443  $where = [ 'rd_from' => $this->getId() ];
1444  $dbw->delete( 'redirect', $where, __METHOD__ );
1445  }
1446 
1447  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1448  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1449  }
1450 
1451  return ( $dbw->affectedRows() != 0 );
1452  }
1453 
1464  public function updateIfNewerOn( $dbw, $revision ) {
1465  $row = $dbw->selectRow(
1466  [ 'revision', 'page' ],
1467  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1468  [
1469  'page_id' => $this->getId(),
1470  'page_latest=rev_id' ],
1471  __METHOD__ );
1472 
1473  if ( $row ) {
1474  if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1475  return false;
1476  }
1477  $prev = $row->rev_id;
1478  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1479  } else {
1480  // No or missing previous revision; mark the page as new
1481  $prev = 0;
1482  $lastRevIsRedirect = null;
1483  }
1484 
1485  $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1486 
1487  return $ret;
1488  }
1489 
1502  public static function hasDifferencesOutsideMainSlot( Revision $a, Revision $b ) {
1503  $aSlots = $a->getRevisionRecord()->getSlots();
1504  $bSlots = $b->getRevisionRecord()->getSlots();
1505  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1506 
1507  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1508  }
1509 
1521  public function getUndoContent( Revision $undo, Revision $undoafter ) {
1522  // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1523 
1524  if ( self::hasDifferencesOutsideMainSlot( $undo, $undoafter ) ) {
1525  // Cannot yet undo edits that involve anything other the main slot.
1526  return false;
1527  }
1528 
1529  $handler = $undo->getContentHandler();
1530  return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1531  }
1532 
1543  public function supportsSections() {
1544  return $this->getContentHandler()->supportsSections();
1545  }
1546 
1561  public function replaceSectionContent(
1562  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1563  ) {
1564  $baseRevId = null;
1565  if ( $edittime && $sectionId !== 'new' ) {
1566  $lb = $this->getDBLoadBalancer();
1567  $dbr = $lb->getConnection( DB_REPLICA );
1568  $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1569  // Try the master if this thread may have just added it.
1570  // This could be abstracted into a Revision method, but we don't want
1571  // to encourage loading of revisions by timestamp.
1572  if ( !$rev
1573  && $lb->getServerCount() > 1
1574  && $lb->hasOrMadeRecentMasterChanges()
1575  ) {
1576  $dbw = $lb->getConnection( DB_MASTER );
1577  $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1578  }
1579  if ( $rev ) {
1580  $baseRevId = $rev->getId();
1581  }
1582  }
1583 
1584  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1585  }
1586 
1600  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1601  $sectionTitle = '', $baseRevId = null
1602  ) {
1603  if ( strval( $sectionId ) === '' ) {
1604  // Whole-page edit; let the whole text through
1605  $newContent = $sectionContent;
1606  } else {
1607  if ( !$this->supportsSections() ) {
1608  throw new MWException( "sections not supported for content model " .
1609  $this->getContentHandler()->getModelID() );
1610  }
1611 
1612  // T32711: always use current version when adding a new section
1613  if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1614  $oldContent = $this->getContent();
1615  } else {
1616  $rev = Revision::newFromId( $baseRevId );
1617  if ( !$rev ) {
1618  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1619  $this->getId() . "; section: $sectionId)\n" );
1620  return null;
1621  }
1622 
1623  $oldContent = $rev->getContent();
1624  }
1625 
1626  if ( !$oldContent ) {
1627  wfDebug( __METHOD__ . ": no page text\n" );
1628  return null;
1629  }
1630 
1631  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1632  }
1633 
1634  return $newContent;
1635  }
1636 
1646  public function checkFlags( $flags ) {
1647  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1648  if ( $this->exists() ) {
1649  $flags |= EDIT_UPDATE;
1650  } else {
1651  $flags |= EDIT_NEW;
1652  }
1653  }
1654 
1655  return $flags;
1656  }
1657 
1661  private function newDerivedDataUpdater() {
1663 
1665  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1666  $this->getRevisionStore(),
1667  $this->getRevisionRenderer(),
1668  $this->getParserCache(),
1671  MediaWikiServices::getInstance()->getContentLanguage(),
1672  MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
1673  );
1674 
1675  $derivedDataUpdater->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership );
1676  $derivedDataUpdater->setArticleCountMethod( $wgArticleCountMethod );
1677 
1678  return $derivedDataUpdater;
1679  }
1680 
1708  private function getDerivedDataUpdater(
1709  User $forUser = null,
1710  RevisionRecord $forRevision = null,
1711  RevisionSlotsUpdate $forUpdate = null,
1712  $forEdit = false
1713  ) {
1714  if ( !$forRevision && !$forUpdate ) {
1715  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1716  // going to use it with.
1717  $this->derivedDataUpdater = null;
1718  }
1719 
1720  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1721  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1722  // to it did not yet initialize it, because we don't know what data it will be
1723  // initialized with.
1724  $this->derivedDataUpdater = null;
1725  }
1726 
1727  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1728  // However, there is no good way to construct a cache key. We'd need to check against all
1729  // cached instances.
1730 
1731  if ( $this->derivedDataUpdater
1732  && !$this->derivedDataUpdater->isReusableFor(
1733  $forUser,
1734  $forRevision,
1735  $forUpdate,
1736  $forEdit ? $this->getLatest() : null
1737  )
1738  ) {
1739  $this->derivedDataUpdater = null;
1740  }
1741 
1742  if ( !$this->derivedDataUpdater ) {
1743  $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1744  }
1745 
1747  }
1748 
1764  public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1766 
1767  $pageUpdater = new PageUpdater(
1768  $user,
1769  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1770  $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1771  $this->getDBLoadBalancer(),
1772  $this->getRevisionStore()
1773  );
1774 
1775  $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
1776  $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
1777  $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
1778 
1779  return $pageUpdater;
1780  }
1781 
1844  public function doEditContent(
1845  Content $content, $summary, $flags = 0, $originalRevId = false,
1846  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1847  ) {
1848  global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1849 
1850  if ( !( $summary instanceof CommentStoreComment ) ) {
1851  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1852  }
1853 
1854  if ( !$user ) {
1855  $user = $wgUser;
1856  }
1857 
1858  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1859  // Checking the minoredit right should be done in the same place the 'bot' right is
1860  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1861  if ( ( $flags & EDIT_MINOR ) && !$user->isAllowed( 'minoredit' ) ) {
1862  $flags = ( $flags & ~EDIT_MINOR );
1863  }
1864 
1865  $slotsUpdate = new RevisionSlotsUpdate();
1866  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1867 
1868  // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1869  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1870  // used by this PageUpdater. However, there is no guarantee for this.
1871  $updater = $this->newPageUpdater( $user, $slotsUpdate );
1872  $updater->setContent( SlotRecord::MAIN, $content );
1873  $updater->setOriginalRevisionId( $originalRevId );
1874  $updater->setUndidRevisionId( $undidRevId );
1875 
1876  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1877 
1878  // TODO: this logic should not be in the storage layer, it's here for compatibility
1879  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1880  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1881  if ( $needsPatrol && $this->getTitle()->userCan( 'autopatrol', $user ) ) {
1882  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1883  }
1884 
1885  $updater->addTags( $tags );
1886 
1887  $revRec = $updater->saveRevision(
1888  $summary,
1889  $flags
1890  );
1891 
1892  // $revRec will be null if the edit failed, or if no new revision was created because
1893  // the content did not change.
1894  if ( $revRec ) {
1895  // update cached fields
1896  // TODO: this is currently redundant to what is done in updateRevisionOn.
1897  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1898  $this->setLastEdit( new Revision( $revRec ) ); // TODO: use RevisionRecord
1899  $this->mLatest = $revRec->getId();
1900  }
1901 
1902  return $updater->getStatus();
1903  }
1904 
1919  public function makeParserOptions( $context ) {
1921 
1922  if ( $this->getTitle()->isConversionTable() ) {
1923  // @todo ConversionTable should become a separate content model, so
1924  // we don't need special cases like this one.
1925  $options->disableContentConversion();
1926  }
1927 
1928  return $options;
1929  }
1930 
1951  public function prepareContentForEdit(
1952  Content $content,
1953  $revision = null,
1954  User $user = null,
1955  $serialFormat = null,
1956  $useCache = true
1957  ) {
1958  global $wgUser;
1959 
1960  if ( !$user ) {
1961  $user = $wgUser;
1962  }
1963 
1964  if ( !is_object( $revision ) ) {
1965  $revid = $revision;
1966  // This code path is deprecated, and nothing is known to
1967  // use it, so performance here shouldn't be a worry.
1968  if ( $revid !== null ) {
1969  wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
1970  $store = $this->getRevisionStore();
1971  $revision = $store->getRevisionById( $revid, Revision::READ_LATEST );
1972  } else {
1973  $revision = null;
1974  }
1975  } elseif ( $revision instanceof Revision ) {
1976  $revision = $revision->getRevisionRecord();
1977  }
1978 
1979  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
1980  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
1981 
1982  if ( !$updater->isUpdatePrepared() ) {
1983  $updater->prepareContent( $user, $slots, $useCache );
1984 
1985  if ( $revision ) {
1986  $updater->prepareUpdate(
1987  $revision,
1988  [
1989  'causeAction' => 'prepare-edit',
1990  'causeAgent' => $user->getName(),
1991  ]
1992  );
1993  }
1994  }
1995 
1996  return $updater->getPreparedEdit();
1997  }
1998 
2026  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2027  $options += [
2028  'causeAction' => 'edit-page',
2029  'causeAgent' => $user->getName(),
2030  ];
2031 
2032  $revision = $revision->getRevisionRecord();
2033 
2034  $updater = $this->getDerivedDataUpdater( $user, $revision );
2035 
2036  $updater->prepareUpdate( $revision, $options );
2037 
2038  $updater->doUpdates();
2039  }
2040 
2054  public function updateParserCache( array $options = [] ) {
2055  $revision = $this->getRevisionRecord();
2056  if ( !$revision || !$revision->getId() ) {
2057  LoggerFactory::getInstance( 'wikipage' )->info(
2058  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2059  );
2060  return;
2061  }
2062  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2063 
2064  $updater = $this->getDerivedDataUpdater( $user, $revision );
2065  $updater->prepareUpdate( $revision, $options );
2066  $updater->doParserCacheUpdate();
2067  }
2068 
2095  public function doSecondaryDataUpdates( array $options = [] ) {
2096  $options['recursive'] = $options['recursive'] ?? true;
2097  $revision = $this->getRevisionRecord();
2098  if ( !$revision || !$revision->getId() ) {
2099  LoggerFactory::getInstance( 'wikipage' )->info(
2100  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2101  );
2102  return;
2103  }
2104  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2105 
2106  $updater = $this->getDerivedDataUpdater( $user, $revision );
2107  $updater->prepareUpdate( $revision, $options );
2108  $updater->doSecondaryDataUpdates( $options );
2109  }
2110 
2125  public function doUpdateRestrictions( array $limit, array $expiry,
2126  &$cascade, $reason, User $user, $tags = null
2127  ) {
2129 
2130  if ( wfReadOnly() ) {
2131  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2132  }
2133 
2134  $this->loadPageData( 'fromdbmaster' );
2135  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2136  $id = $this->getId();
2137 
2138  if ( !$cascade ) {
2139  $cascade = false;
2140  }
2141 
2142  // Take this opportunity to purge out expired restrictions
2144 
2145  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2146  // we expect a single selection, but the schema allows otherwise.
2147  $isProtected = false;
2148  $protect = false;
2149  $changed = false;
2150 
2151  $dbw = wfGetDB( DB_MASTER );
2152 
2153  foreach ( $restrictionTypes as $action ) {
2154  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2155  $expiry[$action] = 'infinity';
2156  }
2157  if ( !isset( $limit[$action] ) ) {
2158  $limit[$action] = '';
2159  } elseif ( $limit[$action] != '' ) {
2160  $protect = true;
2161  }
2162 
2163  // Get current restrictions on $action
2164  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2165  if ( $current != '' ) {
2166  $isProtected = true;
2167  }
2168 
2169  if ( $limit[$action] != $current ) {
2170  $changed = true;
2171  } elseif ( $limit[$action] != '' ) {
2172  // Only check expiry change if the action is actually being
2173  // protected, since expiry does nothing on an not-protected
2174  // action.
2175  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2176  $changed = true;
2177  }
2178  }
2179  }
2180 
2181  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2182  $changed = true;
2183  }
2184 
2185  // If nothing has changed, do nothing
2186  if ( !$changed ) {
2187  return Status::newGood();
2188  }
2189 
2190  if ( !$protect ) { // No protection at all means unprotection
2191  $revCommentMsg = 'unprotectedarticle-comment';
2192  $logAction = 'unprotect';
2193  } elseif ( $isProtected ) {
2194  $revCommentMsg = 'modifiedarticleprotection-comment';
2195  $logAction = 'modify';
2196  } else {
2197  $revCommentMsg = 'protectedarticle-comment';
2198  $logAction = 'protect';
2199  }
2200 
2201  $logRelationsValues = [];
2202  $logRelationsField = null;
2203  $logParamsDetails = [];
2204 
2205  // Null revision (used for change tag insertion)
2206  $nullRevision = null;
2207 
2208  if ( $id ) { // Protection of existing page
2209  // Avoid PHP 7.1 warning of passing $this by reference
2210  $wikiPage = $this;
2211 
2212  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2213  return Status::newGood();
2214  }
2215 
2216  // Only certain restrictions can cascade...
2217  $editrestriction = isset( $limit['edit'] )
2218  ? [ $limit['edit'] ]
2219  : $this->mTitle->getRestrictions( 'edit' );
2220  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2221  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2222  }
2223  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2224  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2225  }
2226 
2227  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2228  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2229  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2230  }
2231  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2232  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2233  }
2234 
2235  // The schema allows multiple restrictions
2236  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2237  $cascade = false;
2238  }
2239 
2240  // insert null revision to identify the page protection change as edit summary
2241  $latest = $this->getLatest();
2242  $nullRevision = $this->insertProtectNullRevision(
2243  $revCommentMsg,
2244  $limit,
2245  $expiry,
2246  $cascade,
2247  $reason,
2248  $user
2249  );
2250 
2251  if ( $nullRevision === null ) {
2252  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2253  }
2254 
2255  $logRelationsField = 'pr_id';
2256 
2257  // Update restrictions table
2258  foreach ( $limit as $action => $restrictions ) {
2259  $dbw->delete(
2260  'page_restrictions',
2261  [
2262  'pr_page' => $id,
2263  'pr_type' => $action
2264  ],
2265  __METHOD__
2266  );
2267  if ( $restrictions != '' ) {
2268  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2269  $dbw->insert(
2270  'page_restrictions',
2271  [
2272  'pr_page' => $id,
2273  'pr_type' => $action,
2274  'pr_level' => $restrictions,
2275  'pr_cascade' => $cascadeValue,
2276  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2277  ],
2278  __METHOD__
2279  );
2280  $logRelationsValues[] = $dbw->insertId();
2281  $logParamsDetails[] = [
2282  'type' => $action,
2283  'level' => $restrictions,
2284  'expiry' => $expiry[$action],
2285  'cascade' => (bool)$cascadeValue,
2286  ];
2287  }
2288  }
2289 
2290  // Clear out legacy restriction fields
2291  $dbw->update(
2292  'page',
2293  [ 'page_restrictions' => '' ],
2294  [ 'page_id' => $id ],
2295  __METHOD__
2296  );
2297 
2298  // Avoid PHP 7.1 warning of passing $this by reference
2299  $wikiPage = $this;
2300 
2301  Hooks::run( 'NewRevisionFromEditComplete',
2302  [ $this, $nullRevision, $latest, $user ] );
2303  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2304  } else { // Protection of non-existing page (also known as "title protection")
2305  // Cascade protection is meaningless in this case
2306  $cascade = false;
2307 
2308  if ( $limit['create'] != '' ) {
2309  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2310  $dbw->replace( 'protected_titles',
2311  [ [ 'pt_namespace', 'pt_title' ] ],
2312  [
2313  'pt_namespace' => $this->mTitle->getNamespace(),
2314  'pt_title' => $this->mTitle->getDBkey(),
2315  'pt_create_perm' => $limit['create'],
2316  'pt_timestamp' => $dbw->timestamp(),
2317  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2318  'pt_user' => $user->getId(),
2319  ] + $commentFields, __METHOD__
2320  );
2321  $logParamsDetails[] = [
2322  'type' => 'create',
2323  'level' => $limit['create'],
2324  'expiry' => $expiry['create'],
2325  ];
2326  } else {
2327  $dbw->delete( 'protected_titles',
2328  [
2329  'pt_namespace' => $this->mTitle->getNamespace(),
2330  'pt_title' => $this->mTitle->getDBkey()
2331  ], __METHOD__
2332  );
2333  }
2334  }
2335 
2336  $this->mTitle->flushRestrictions();
2337  InfoAction::invalidateCache( $this->mTitle );
2338 
2339  if ( $logAction == 'unprotect' ) {
2340  $params = [];
2341  } else {
2342  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2343  $params = [
2344  '4::description' => $protectDescriptionLog, // parameter for IRC
2345  '5:bool:cascade' => $cascade,
2346  'details' => $logParamsDetails, // parameter for localize and api
2347  ];
2348  }
2349 
2350  // Update the protection log
2351  $logEntry = new ManualLogEntry( 'protect', $logAction );
2352  $logEntry->setTarget( $this->mTitle );
2353  $logEntry->setComment( $reason );
2354  $logEntry->setPerformer( $user );
2355  $logEntry->setParameters( $params );
2356  if ( !is_null( $nullRevision ) ) {
2357  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2358  }
2359  $logEntry->setTags( $tags );
2360  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2361  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2362  }
2363  $logId = $logEntry->insert();
2364  $logEntry->publish( $logId );
2365 
2366  return Status::newGood( $logId );
2367  }
2368 
2380  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2381  array $expiry, $cascade, $reason, $user = null
2382  ) {
2383  $dbw = wfGetDB( DB_MASTER );
2384 
2385  // Prepare a null revision to be added to the history
2386  $editComment = wfMessage(
2387  $revCommentMsg,
2388  $this->mTitle->getPrefixedText(),
2389  $user ? $user->getName() : ''
2390  )->inContentLanguage()->text();
2391  if ( $reason ) {
2392  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2393  }
2394  $protectDescription = $this->protectDescription( $limit, $expiry );
2395  if ( $protectDescription ) {
2396  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2397  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2398  ->inContentLanguage()->text();
2399  }
2400  if ( $cascade ) {
2401  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2402  $editComment .= wfMessage( 'brackets' )->params(
2403  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2404  )->inContentLanguage()->text();
2405  }
2406 
2407  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2408  if ( $nullRev ) {
2409  $nullRev->insertOn( $dbw );
2410 
2411  // Update page record and touch page
2412  $oldLatest = $nullRev->getParentId();
2413  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2414  }
2415 
2416  return $nullRev;
2417  }
2418 
2423  protected function formatExpiry( $expiry ) {
2424  if ( $expiry != 'infinity' ) {
2425  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2426  return wfMessage(
2427  'protect-expiring',
2428  $contLang->timeanddate( $expiry, false, false ),
2429  $contLang->date( $expiry, false, false ),
2430  $contLang->time( $expiry, false, false )
2431  )->inContentLanguage()->text();
2432  } else {
2433  return wfMessage( 'protect-expiry-indefinite' )
2434  ->inContentLanguage()->text();
2435  }
2436  }
2437 
2445  public function protectDescription( array $limit, array $expiry ) {
2446  $protectDescription = '';
2447 
2448  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2449  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2450  # All possible message keys are listed here for easier grepping:
2451  # * restriction-create
2452  # * restriction-edit
2453  # * restriction-move
2454  # * restriction-upload
2455  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2456  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2457  # with '' filtered out. All possible message keys are listed below:
2458  # * protect-level-autoconfirmed
2459  # * protect-level-sysop
2460  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2461  ->inContentLanguage()->text();
2462 
2463  $expiryText = $this->formatExpiry( $expiry[$action] );
2464 
2465  if ( $protectDescription !== '' ) {
2466  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2467  }
2468  $protectDescription .= wfMessage( 'protect-summary-desc' )
2469  ->params( $actionText, $restrictionsText, $expiryText )
2470  ->inContentLanguage()->text();
2471  }
2472 
2473  return $protectDescription;
2474  }
2475 
2487  public function protectDescriptionLog( array $limit, array $expiry ) {
2488  $protectDescriptionLog = '';
2489 
2490  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2491  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2492  $expiryText = $this->formatExpiry( $expiry[$action] );
2493  $protectDescriptionLog .=
2494  $dirMark .
2495  "[$action=$restrictions] ($expiryText)";
2496  }
2497 
2498  return trim( $protectDescriptionLog );
2499  }
2500 
2510  protected static function flattenRestrictions( $limit ) {
2511  if ( !is_array( $limit ) ) {
2512  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2513  }
2514 
2515  $bits = [];
2516  ksort( $limit );
2517 
2518  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2519  $bits[] = "$action=$restrictions";
2520  }
2521 
2522  return implode( ':', $bits );
2523  }
2524 
2537  public function isBatchedDelete( $safetyMargin = 0 ) {
2539 
2540  $dbr = wfGetDB( DB_REPLICA );
2541  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2542  $revCount += $safetyMargin;
2543 
2544  return $revCount >= $wgDeleteRevisionsBatchSize;
2545  }
2546 
2566  public function doDeleteArticle(
2567  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2568  $immediate = false
2569  ) {
2570  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2571  [], 'delete', $immediate );
2572 
2573  // Returns true if the page was actually deleted, or is scheduled for deletion
2574  return $status->isOK();
2575  }
2576 
2599  public function doDeleteArticleReal(
2600  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2601  $tags = [], $logsubtype = 'delete', $immediate = false
2602  ) {
2603  global $wgUser;
2604 
2605  wfDebug( __METHOD__ . "\n" );
2606 
2608 
2609  // Avoid PHP 7.1 warning of passing $this by reference
2610  $wikiPage = $this;
2611 
2612  $deleter = is_null( $deleter ) ? $wgUser : $deleter;
2613  if ( !Hooks::run( 'ArticleDelete',
2614  [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2615  ) ) {
2616  if ( $status->isOK() ) {
2617  // Hook aborted but didn't set a fatal status
2618  $status->fatal( 'delete-hook-aborted' );
2619  }
2620  return $status;
2621  }
2622 
2623  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2624  $logsubtype, $immediate );
2625  }
2626 
2635  public function doDeleteArticleBatched(
2636  $reason, $suppress, User $deleter, $tags,
2637  $logsubtype, $immediate = false, $webRequestId = null
2638  ) {
2639  wfDebug( __METHOD__ . "\n" );
2640 
2642 
2643  $dbw = wfGetDB( DB_MASTER );
2644  $dbw->startAtomic( __METHOD__ );
2645 
2646  $this->loadPageData( self::READ_LATEST );
2647  $id = $this->getId();
2648  // T98706: lock the page from various other updates but avoid using
2649  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2650  // the revisions queries (which also JOIN on user). Only lock the page
2651  // row and CAS check on page_latest to see if the trx snapshot matches.
2652  $lockedLatest = $this->lockAndGetLatest();
2653  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2654  $dbw->endAtomic( __METHOD__ );
2655  // Page not there or trx snapshot is stale
2656  $status->error( 'cannotdelete',
2657  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2658  return $status;
2659  }
2660 
2661  // At this point we are now committed to returning an OK
2662  // status unless some DB query error or other exception comes up.
2663  // This way callers don't have to call rollback() if $status is bad
2664  // unless they actually try to catch exceptions (which is rare).
2665 
2666  // we need to remember the old content so we can use it to generate all deletion updates.
2667  $revision = $this->getRevision();
2668  try {
2669  $content = $this->getContent( Revision::RAW );
2670  } catch ( Exception $ex ) {
2671  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2672  . $ex->getMessage() );
2673 
2674  $content = null;
2675  }
2676 
2677  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2678  // one batch of revisions and defer archival of any others to the job queue.
2679  $explictTrxLogged = false;
2680  while ( true ) {
2681  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2682  if ( $done || !$immediate ) {
2683  break;
2684  }
2685  $dbw->endAtomic( __METHOD__ );
2686  if ( $dbw->explicitTrxActive() ) {
2687  // Explict transactions may never happen here in practice. Log to be sure.
2688  if ( !$explictTrxLogged ) {
2689  $explictTrxLogged = true;
2690  LoggerFactory::getInstance( 'wfDebug' )->debug(
2691  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2692  'title' => $this->getTitle()->getText(),
2693  ] );
2694  }
2695  continue;
2696  }
2697  if ( $dbw->trxLevel() ) {
2698  $dbw->commit();
2699  }
2700  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2701  $lbFactory->waitForReplication();
2702  $dbw->startAtomic( __METHOD__ );
2703  }
2704 
2705  // If done archiving, also delete the article.
2706  if ( !$done ) {
2707  $dbw->endAtomic( __METHOD__ );
2708 
2709  $jobParams = [
2710  'wikiPageId' => $id,
2711  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2712  'reason' => $reason,
2713  'suppress' => $suppress,
2714  'userId' => $deleter->getId(),
2715  'tags' => json_encode( $tags ),
2716  'logsubtype' => $logsubtype,
2717  ];
2718 
2719  $job = new DeletePageJob( $this->getTitle(), $jobParams );
2720  JobQueueGroup::singleton()->push( $job );
2721 
2722  $status->warning( 'delete-scheduled',
2723  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2724  } else {
2725  // Get archivedRevisionCount by db query, because there's no better alternative.
2726  // Jobs cannot pass a count of archived revisions to the next job, because additional
2727  // deletion operations can be started while the first is running. Jobs from each
2728  // gracefully interleave, but would not know about each other's count. Deduplication
2729  // in the job queue to avoid simultaneous deletion operations would add overhead.
2730  // Number of archived revisions cannot be known beforehand, because edits can be made
2731  // while deletion operations are being processed, changing the number of archivals.
2732  $archivedRevisionCount = $dbw->selectField(
2733  'archive', 'COUNT(*)',
2734  [
2735  'ar_namespace' => $this->getTitle()->getNamespace(),
2736  'ar_title' => $this->getTitle()->getDBkey(),
2737  'ar_page_id' => $id
2738  ], __METHOD__
2739  );
2740 
2741  // Clone the title and wikiPage, so we have the information we need when
2742  // we log and run the ArticleDeleteComplete hook.
2743  $logTitle = clone $this->mTitle;
2744  $wikiPageBeforeDelete = clone $this;
2745 
2746  // Now that it's safely backed up, delete it
2747  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2748 
2749  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2750  $logtype = $suppress ? 'suppress' : 'delete';
2751 
2752  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2753  $logEntry->setPerformer( $deleter );
2754  $logEntry->setTarget( $logTitle );
2755  $logEntry->setComment( $reason );
2756  $logEntry->setTags( $tags );
2757  $logid = $logEntry->insert();
2758 
2759  $dbw->onTransactionPreCommitOrIdle(
2760  function () use ( $logEntry, $logid ) {
2761  // T58776: avoid deadlocks (especially from FileDeleteForm)
2762  $logEntry->publish( $logid );
2763  },
2764  __METHOD__
2765  );
2766 
2767  $dbw->endAtomic( __METHOD__ );
2768 
2769  $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2770 
2771  Hooks::run( 'ArticleDeleteComplete', [
2772  &$wikiPageBeforeDelete,
2773  &$deleter,
2774  $reason,
2775  $id,
2776  $content,
2777  $logEntry,
2778  $archivedRevisionCount
2779  ] );
2780  $status->value = $logid;
2781 
2782  // Show log excerpt on 404 pages rather than just a link
2783  $cache = MediaWikiServices::getInstance()->getMainObjectStash();
2784  $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2785  $cache->set( $key, 1, $cache::TTL_DAY );
2786  }
2787 
2788  return $status;
2789  }
2790 
2800  protected function archiveRevisions( $dbw, $id, $suppress ) {
2804 
2805  // Given the lock above, we can be confident in the title and page ID values
2806  $namespace = $this->getTitle()->getNamespace();
2807  $dbKey = $this->getTitle()->getDBkey();
2808 
2809  $commentStore = CommentStore::getStore();
2810  $actorMigration = ActorMigration::newMigration();
2811 
2813  $bitfield = false;
2814 
2815  // Bitfields to further suppress the content
2816  if ( $suppress ) {
2817  $bitfield = Revision::SUPPRESSED_ALL;
2818  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2819  }
2820 
2821  // For now, shunt the revision data into the archive table.
2822  // Text is *not* removed from the text table; bulk storage
2823  // is left intact to avoid breaking block-compression or
2824  // immutable storage schemes.
2825  // In the future, we may keep revisions and mark them with
2826  // the rev_deleted field, which is reserved for this purpose.
2827 
2828  // Lock rows in `revision` and its temp tables, but not any others.
2829  // Note array_intersect() preserves keys from the first arg, and we're
2830  // assuming $revQuery has `revision` primary and isn't using subtables
2831  // for anything we care about.
2832  $dbw->lockForUpdate(
2833  array_intersect(
2834  $revQuery['tables'],
2835  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2836  ),
2837  [ 'rev_page' => $id ],
2838  __METHOD__,
2839  [],
2840  $revQuery['joins']
2841  );
2842 
2843  // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
2844  // so we can copy it to the archive table.
2845  // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
2846  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2847  $revQuery['fields'][] = 'rev_text_id';
2848 
2849  if ( $wgContentHandlerUseDB ) {
2850  $revQuery['fields'][] = 'rev_content_model';
2851  $revQuery['fields'][] = 'rev_content_format';
2852  }
2853  }
2854 
2855  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2856  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2857  $res = $dbw->select(
2858  $revQuery['tables'],
2859  $revQuery['fields'],
2860  [ 'rev_page' => $id ],
2861  __METHOD__,
2862  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2863  $revQuery['joins']
2864  );
2865 
2866  // Build their equivalent archive rows
2867  $rowsInsert = [];
2868  $revids = [];
2869 
2871  $ipRevIds = [];
2872 
2873  $done = true;
2874  foreach ( $res as $row ) {
2875  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2876  $done = false;
2877  break;
2878  }
2879 
2880  $comment = $commentStore->getComment( 'rev_comment', $row );
2881  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2882  $rowInsert = [
2883  'ar_namespace' => $namespace,
2884  'ar_title' => $dbKey,
2885  'ar_timestamp' => $row->rev_timestamp,
2886  'ar_minor_edit' => $row->rev_minor_edit,
2887  'ar_rev_id' => $row->rev_id,
2888  'ar_parent_id' => $row->rev_parent_id,
2897  'ar_len' => $row->rev_len,
2898  'ar_page_id' => $id,
2899  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2900  'ar_sha1' => $row->rev_sha1,
2901  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2902  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2903 
2904  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2905  $rowInsert['ar_text_id'] = $row->rev_text_id;
2906 
2907  if ( $wgContentHandlerUseDB ) {
2908  $rowInsert['ar_content_model'] = $row->rev_content_model;
2909  $rowInsert['ar_content_format'] = $row->rev_content_format;
2910  }
2911  }
2912 
2913  $rowsInsert[] = $rowInsert;
2914  $revids[] = $row->rev_id;
2915 
2916  // Keep track of IP edits, so that the corresponding rows can
2917  // be deleted in the ip_changes table.
2918  if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2919  $ipRevIds[] = $row->rev_id;
2920  }
2921  }
2922 
2923  // This conditional is just a sanity check
2924  if ( count( $revids ) > 0 ) {
2925  // Copy them into the archive table
2926  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2927 
2928  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2929  if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
2930  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2931  }
2932  if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
2933  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2934  }
2935 
2936  // Also delete records from ip_changes as applicable.
2937  if ( count( $ipRevIds ) > 0 ) {
2938  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2939  }
2940  }
2941 
2942  return $done;
2943  }
2944 
2951  public function lockAndGetLatest() {
2952  return (int)wfGetDB( DB_MASTER )->selectField(
2953  'page',
2954  'page_latest',
2955  [
2956  'page_id' => $this->getId(),
2957  // Typically page_id is enough, but some code might try to do
2958  // updates assuming the title is the same, so verify that
2959  'page_namespace' => $this->getTitle()->getNamespace(),
2960  'page_title' => $this->getTitle()->getDBkey()
2961  ],
2962  __METHOD__,
2963  [ 'FOR UPDATE' ]
2964  );
2965  }
2966 
2979  public function doDeleteUpdates(
2980  $id, Content $content = null, Revision $revision = null, User $user = null
2981  ) {
2982  if ( $id !== $this->getId() ) {
2983  throw new InvalidArgumentException( 'Mismatching page ID' );
2984  }
2985 
2986  try {
2987  $countable = $this->isCountable();
2988  } catch ( Exception $ex ) {
2989  // fallback for deleting broken pages for which we cannot load the content for
2990  // some reason. Note that doDeleteArticleReal() already logged this problem.
2991  $countable = false;
2992  }
2993 
2994  // Update site status
2996  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
2997  ) );
2998 
2999  // Delete pagelinks, update secondary indexes, etc
3000  $updates = $this->getDeletionUpdates(
3001  $revision ? $revision->getRevisionRecord() : $content
3002  );
3003  foreach ( $updates as $update ) {
3004  DeferredUpdates::addUpdate( $update );
3005  }
3006 
3007  $causeAgent = $user ? $user->getName() : 'unknown';
3008  // Reparse any pages transcluding this page
3010  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3011  // Reparse any pages including this image
3012  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3014  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3015  }
3016 
3017  // Clear caches
3018  self::onArticleDelete( $this->mTitle );
3020  $this->mTitle, $revision, null, wfWikiID()
3021  );
3022 
3023  // Reset this object and the Title object
3024  $this->loadFromRow( false, self::READ_LATEST );
3025 
3026  // Search engine
3027  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3028  }
3029 
3059  public function doRollback(
3060  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3061  ) {
3062  $resultDetails = null;
3063 
3064  // Check permissions
3065  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3066  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3067  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3068 
3069  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3070  $errors[] = [ 'sessionfailure' ];
3071  }
3072 
3073  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3074  $errors[] = [ 'actionthrottledtext' ];
3075  }
3076 
3077  // If there were errors, bail out now
3078  if ( !empty( $errors ) ) {
3079  return $errors;
3080  }
3081 
3082  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3083  }
3084 
3105  public function commitRollback( $fromP, $summary, $bot,
3106  &$resultDetails, User $guser, $tags = null
3107  ) {
3108  global $wgUseRCPatrol;
3109 
3110  $dbw = wfGetDB( DB_MASTER );
3111 
3112  if ( wfReadOnly() ) {
3113  return [ [ 'readonlytext' ] ];
3114  }
3115 
3116  // Begin revision creation cycle by creating a PageUpdater.
3117  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3118  $updater = $this->newPageUpdater( $guser );
3119  $current = $updater->grabParentRevision();
3120 
3121  if ( is_null( $current ) ) {
3122  // Something wrong... no page?
3123  return [ [ 'notanarticle' ] ];
3124  }
3125 
3126  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3127  $legacyCurrent = new Revision( $current );
3128  $from = str_replace( '_', ' ', $fromP );
3129 
3130  // User name given should match up with the top revision.
3131  // If the revision's user is not visible, then $from should be empty.
3132  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3133  $resultDetails = [ 'current' => $legacyCurrent ];
3134  return [ [ 'alreadyrolled',
3135  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3136  htmlspecialchars( $fromP ),
3137  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3138  ] ];
3139  }
3140 
3141  // Get the last edit not by this person...
3142  // Note: these may not be public values
3143  $actorWhere = ActorMigration::newMigration()->getWhere(
3144  $dbw,
3145  'rev_user',
3146  $current->getUser( RevisionRecord::RAW )
3147  );
3148 
3149  $s = $dbw->selectRow(
3150  [ 'revision' ] + $actorWhere['tables'],
3151  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3152  [
3153  'rev_page' => $current->getPageId(),
3154  'NOT(' . $actorWhere['conds'] . ')',
3155  ],
3156  __METHOD__,
3157  [
3158  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3159  'ORDER BY' => 'rev_timestamp DESC'
3160  ],
3161  $actorWhere['joins']
3162  );
3163  if ( $s === false ) {
3164  // No one else ever edited this page
3165  return [ [ 'cantrollback' ] ];
3166  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3167  || $s->rev_deleted & RevisionRecord::DELETED_USER
3168  ) {
3169  // Only admins can see this text
3170  return [ [ 'notvisiblerev' ] ];
3171  }
3172 
3173  // Generate the edit summary if necessary
3174  $target = $this->getRevisionStore()->getRevisionById(
3175  $s->rev_id,
3176  RevisionStore::READ_LATEST
3177  );
3178  if ( empty( $summary ) ) {
3179  if ( !$currentEditorForPublic ) { // no public user name
3180  $summary = wfMessage( 'revertpage-nouser' );
3181  } else {
3182  $summary = wfMessage( 'revertpage' );
3183  }
3184  }
3185  $legacyTarget = new Revision( $target );
3186  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3187 
3188  // Allow the custom summary to use the same args as the default message
3189  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3190  $args = [
3191  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3192  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3193  $s->rev_id,
3194  $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3195  $current->getId(),
3196  $contLang->timeanddate( $current->getTimestamp() )
3197  ];
3198  if ( $summary instanceof Message ) {
3199  $summary = $summary->params( $args )->inContentLanguage()->text();
3200  } else {
3201  $summary = wfMsgReplaceArgs( $summary, $args );
3202  }
3203 
3204  // Trim spaces on user supplied text
3205  $summary = trim( $summary );
3206 
3207  // Save
3208  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3209 
3210  if ( $guser->isAllowed( 'minoredit' ) ) {
3211  $flags |= EDIT_MINOR;
3212  }
3213 
3214  if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3215  $flags |= EDIT_FORCE_BOT;
3216  }
3217 
3218  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3219  $currentContent = $current->getContent( SlotRecord::MAIN );
3220  $targetContent = $target->getContent( SlotRecord::MAIN );
3221  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3222 
3223  if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3224  $tags[] = 'mw-rollback';
3225  }
3226 
3227  // Build rollback revision:
3228  // Restore old content
3229  // TODO: MCR: test this once we can store multiple slots
3230  foreach ( $target->getSlots()->getSlots() as $slot ) {
3231  $updater->inheritSlot( $slot );
3232  }
3233 
3234  // Remove extra slots
3235  // TODO: MCR: test this once we can store multiple slots
3236  foreach ( $current->getSlotRoles() as $role ) {
3237  if ( !$target->hasSlot( $role ) ) {
3238  $updater->removeSlot( $role );
3239  }
3240  }
3241 
3242  $updater->setOriginalRevisionId( $target->getId() );
3243  // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3244  $updater->addTags( $tags );
3245 
3246  // TODO: this logic should not be in the storage layer, it's here for compatibility
3247  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3248  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3249  if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $guser ) ) {
3250  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3251  }
3252 
3253  // Actually store the rollback
3254  $rev = $updater->saveRevision(
3256  $flags
3257  );
3258 
3259  // Set patrolling and bot flag on the edits, which gets rollbacked.
3260  // This is done even on edit failure to have patrolling in that case (T64157).
3261  $set = [];
3262  if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3263  // Mark all reverted edits as bot
3264  $set['rc_bot'] = 1;
3265  }
3266 
3267  if ( $wgUseRCPatrol ) {
3268  // Mark all reverted edits as patrolled
3269  $set['rc_patrolled'] = RecentChange::PRC_PATROLLED;
3270  }
3271 
3272  if ( count( $set ) ) {
3273  $actorWhere = ActorMigration::newMigration()->getWhere(
3274  $dbw,
3275  'rc_user',
3276  $current->getUser( RevisionRecord::RAW ),
3277  false
3278  );
3279  $dbw->update( 'recentchanges', $set,
3280  [ /* WHERE */
3281  'rc_cur_id' => $current->getPageId(),
3282  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3283  $actorWhere['conds'], // No tables/joins are needed for rc_user
3284  ],
3285  __METHOD__
3286  );
3287  }
3288 
3289  if ( !$updater->wasSuccessful() ) {
3290  return $updater->getStatus()->getErrorsArray();
3291  }
3292 
3293  // Report if the edit was not created because it did not change the content.
3294  if ( $updater->isUnchanged() ) {
3295  $resultDetails = [ 'current' => $legacyCurrent ];
3296  return [ [ 'alreadyrolled',
3297  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3298  htmlspecialchars( $fromP ),
3299  htmlspecialchars( $targetEditorForPublic ? $targetEditorForPublic->getName() : '' )
3300  ] ];
3301  }
3302 
3303  if ( $changingContentModel ) {
3304  // If the content model changed during the rollback,
3305  // make sure it gets logged to Special:Log/contentmodel
3306  $log = new ManualLogEntry( 'contentmodel', 'change' );
3307  $log->setPerformer( $guser );
3308  $log->setTarget( $this->mTitle );
3309  $log->setComment( $summary );
3310  $log->setParameters( [
3311  '4::oldmodel' => $currentContent->getModel(),
3312  '5::newmodel' => $targetContent->getModel(),
3313  ] );
3314 
3315  $logId = $log->insert( $dbw );
3316  $log->publish( $logId );
3317  }
3318 
3319  $revId = $rev->getId();
3320 
3321  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3322 
3323  $resultDetails = [
3324  'summary' => $summary,
3325  'current' => $legacyCurrent,
3326  'target' => $legacyTarget,
3327  'newid' => $revId,
3328  'tags' => $tags
3329  ];
3330 
3331  // TODO: make this return a Status object and wrap $resultDetails in that.
3332  return [];
3333  }
3334 
3346  public static function onArticleCreate( Title $title ) {
3347  // TODO: move this into a PageEventEmitter service
3348 
3349  // Update existence markers on article/talk tabs...
3350  $other = $title->getOtherPage();
3351 
3352  $other->purgeSquid();
3353 
3354  $title->touchLinks();
3355  $title->purgeSquid();
3356  $title->deleteTitleProtection();
3357 
3358  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3359 
3360  // Invalidate caches of articles which include this page
3362  new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
3363  );
3364 
3365  if ( $title->getNamespace() == NS_CATEGORY ) {
3366  // Load the Category object, which will schedule a job to create
3367  // the category table row if necessary. Checking a replica DB is ok
3368  // here, in the worst case it'll run an unnecessary recount job on
3369  // a category that probably doesn't have many members.
3370  Category::newFromTitle( $title )->getID();
3371  }
3372  }
3373 
3379  public static function onArticleDelete( Title $title ) {
3380  // TODO: move this into a PageEventEmitter service
3381 
3382  // Update existence markers on article/talk tabs...
3383  // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3384  BacklinkCache::get( $title )->clear();
3385  $other = $title->getOtherPage();
3386 
3387  $other->purgeSquid();
3388 
3389  $title->touchLinks();
3390  $title->purgeSquid();
3391 
3392  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3393 
3394  // File cache
3396  InfoAction::invalidateCache( $title );
3397 
3398  // Messages
3399  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3400  MessageCache::singleton()->updateMessageOverride( $title, null );
3401  }
3402 
3403  // Images
3404  if ( $title->getNamespace() == NS_FILE ) {
3406  new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
3407  );
3408  }
3409 
3410  // User talk pages
3411  if ( $title->getNamespace() == NS_USER_TALK ) {
3412  $user = User::newFromName( $title->getText(), false );
3413  if ( $user ) {
3414  $user->setNewtalk( false );
3415  }
3416  }
3417 
3418  // Image redirects
3419  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3420 
3421  // Purge cross-wiki cache entities referencing this page
3422  self::purgeInterwikiCheckKey( $title );
3423  }
3424 
3433  public static function onArticleEdit(
3434  Title $title,
3435  Revision $revision = null,
3436  $slotsChanged = null
3437  ) {
3438  // TODO: move this into a PageEventEmitter service
3439 
3440  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3441  // Invalidate caches of articles which include this page.
3442  // Only for the main slot, because only the main slot is transcluded.
3443  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3445  new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
3446  );
3447  }
3448 
3449  // Invalidate the caches of all pages which redirect here
3451  new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
3452  );
3453 
3454  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3455 
3456  // Purge CDN for this page only
3457  $title->purgeSquid();
3458  // Clear file cache for this page only
3460 
3461  // Purge ?action=info cache
3462  $revid = $revision ? $revision->getId() : null;
3463  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3464  InfoAction::invalidateCache( $title, $revid );
3465  } );
3466 
3467  // Purge cross-wiki cache entities referencing this page
3468  self::purgeInterwikiCheckKey( $title );
3469  }
3470 
3478  private static function purgeInterwikiCheckKey( Title $title ) {
3480 
3481  if ( !$wgEnableScaryTranscluding ) {
3482  return; // @todo: perhaps this wiki is only used as a *source* for content?
3483  }
3484 
3485  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3486  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3487  $cache->resetCheckKey(
3488  // Do not include the namespace since there can be multiple aliases to it
3489  // due to different namespace text definitions on different wikis. This only
3490  // means that some cache invalidations happen that are not strictly needed.
3491  $cache->makeGlobalKey(
3492  'interwiki-page',
3493  WikiMap::getCurrentWikiDomain()->getId(),
3494  $title->getDBkey()
3495  )
3496  );
3497  } );
3498  }
3499 
3506  public function getCategories() {
3507  $id = $this->getId();
3508  if ( $id == 0 ) {
3509  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3510  }
3511 
3512  $dbr = wfGetDB( DB_REPLICA );
3513  $res = $dbr->select( 'categorylinks',
3514  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3515  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3516  // as not being aliases, and NS_CATEGORY is numeric
3517  [ 'cl_from' => $id ],
3518  __METHOD__ );
3519 
3520  return TitleArray::newFromResult( $res );
3521  }
3522 
3529  public function getHiddenCategories() {
3530  $result = [];
3531  $id = $this->getId();
3532 
3533  if ( $id == 0 ) {
3534  return [];
3535  }
3536 
3537  $dbr = wfGetDB( DB_REPLICA );
3538  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3539  [ 'cl_to' ],
3540  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3541  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3542  __METHOD__ );
3543 
3544  if ( $res !== false ) {
3545  foreach ( $res as $row ) {
3546  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3547  }
3548  }
3549 
3550  return $result;
3551  }
3552 
3560  public function getAutoDeleteReason( &$hasHistory ) {
3561  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3562  }
3563 
3574  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3575  $id = $id ?: $this->getId();
3576  $type = MWNamespace::getCategoryLinkType( $this->getTitle()->getNamespace() );
3577 
3578  $addFields = [ 'cat_pages = cat_pages + 1' ];
3579  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3580  if ( $type !== 'page' ) {
3581  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3582  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3583  }
3584 
3585  $dbw = wfGetDB( DB_MASTER );
3586 
3587  if ( count( $added ) ) {
3588  $existingAdded = $dbw->selectFieldValues(
3589  'category',
3590  'cat_title',
3591  [ 'cat_title' => $added ],
3592  __METHOD__
3593  );
3594 
3595  // For category rows that already exist, do a plain
3596  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3597  // to avoid creating gaps in the cat_id sequence.
3598  if ( count( $existingAdded ) ) {
3599  $dbw->update(
3600  'category',
3601  $addFields,
3602  [ 'cat_title' => $existingAdded ],
3603  __METHOD__
3604  );
3605  }
3606 
3607  $missingAdded = array_diff( $added, $existingAdded );
3608  if ( count( $missingAdded ) ) {
3609  $insertRows = [];
3610  foreach ( $missingAdded as $cat ) {
3611  $insertRows[] = [
3612  'cat_title' => $cat,
3613  'cat_pages' => 1,
3614  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3615  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3616  ];
3617  }
3618  $dbw->upsert(
3619  'category',
3620  $insertRows,
3621  [ 'cat_title' ],
3622  $addFields,
3623  __METHOD__
3624  );
3625  }
3626  }
3627 
3628  if ( count( $deleted ) ) {
3629  $dbw->update(
3630  'category',
3631  $removeFields,
3632  [ 'cat_title' => $deleted ],
3633  __METHOD__
3634  );
3635  }
3636 
3637  foreach ( $added as $catName ) {
3638  $cat = Category::newFromName( $catName );
3639  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3640  }
3641 
3642  foreach ( $deleted as $catName ) {
3643  $cat = Category::newFromName( $catName );
3644  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3645  // Refresh counts on categories that should be empty now (after commit, T166757)
3646  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3647  $cat->refreshCountsIfEmpty();
3648  } );
3649  }
3650  }
3651 
3658  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3659  if ( wfReadOnly() ) {
3660  return;
3661  }
3662 
3663  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3664  [ $this, $this->mTitle, $parserOutput ]
3665  ) ) {
3666  return;
3667  }
3668 
3669  $config = RequestContext::getMain()->getConfig();
3670 
3671  $params = [
3672  'isOpportunistic' => true,
3673  'rootJobTimestamp' => $parserOutput->getCacheTime()
3674  ];
3675 
3676  if ( $this->mTitle->areRestrictionsCascading() ) {
3677  // If the page is cascade protecting, the links should really be up-to-date
3678  JobQueueGroup::singleton()->lazyPush(
3679  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3680  );
3681  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3682  // Assume the output contains "dynamic" time/random based magic words.
3683  // Only update pages that expired due to dynamic content and NOT due to edits
3684  // to referenced templates/files. When the cache expires due to dynamic content,
3685  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3686  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3687  // template/file edit already triggered recursive RefreshLinksJob jobs.
3688  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3689  // If a page is uncacheable, do not keep spamming a job for it.
3690  // Although it would be de-duplicated, it would still waste I/O.
3692  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3693  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3694  if ( $cache->add( $key, time(), $ttl ) ) {
3695  JobQueueGroup::singleton()->lazyPush(
3696  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3697  );
3698  }
3699  }
3700  }
3701  }
3702 
3712  public function getDeletionUpdates( $rev = null ) {
3713  if ( !$rev ) {
3714  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3715 
3716  try {
3717  $rev = $this->getRevisionRecord();
3718  } catch ( Exception $ex ) {
3719  // If we can't load the content, something is wrong. Perhaps that's why
3720  // the user is trying to delete the page, so let's not fail in that case.
3721  // Note that doDeleteArticleReal() will already have logged an issue with
3722  // loading the content.
3723  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3724  }
3725  }
3726 
3727  if ( !$rev ) {
3728  $slotContent = [];
3729  } elseif ( $rev instanceof Content ) {
3730  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3731 
3732  $slotContent = [ SlotRecord::MAIN => $rev ];
3733  } else {
3734  $slotContent = array_map( function ( SlotRecord $slot ) {
3735  return $slot->getContent( Revision::RAW );
3736  }, $rev->getSlots()->getSlots() );
3737  }
3738 
3739  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3740 
3741  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3742  // model here, not the content object!
3743  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3745  foreach ( $slotContent as $role => $content ) {
3746  $handler = $content->getContentHandler();
3747 
3748  $updates = $handler->getDeletionUpdates(
3749  $this->getTitle(),
3750  $role
3751  );
3752  $allUpdates = array_merge( $allUpdates, $updates );
3753 
3754  // TODO: remove B/C hack in 1.32!
3755  $legacyUpdates = $content->getDeletionUpdates( $this );
3756 
3757  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3758  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3759  return !( $update instanceof LinksDeletionUpdate );
3760  } );
3761 
3762  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3763  }
3764 
3765  Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3766 
3767  // TODO: hard deprecate old hook in 1.33
3768  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3769  return $allUpdates;
3770  }
3771 
3779  public function isLocal() {
3780  return true;
3781  }
3782 
3792  public function getWikiDisplayName() {
3793  global $wgSitename;
3794  return $wgSitename;
3795  }
3796 
3805  public function getSourceURL() {
3806  return $this->getTitle()->getCanonicalURL();
3807  }
3808 
3815  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3816 
3817  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3818  }
3819 
3820 }
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:798
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:681
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:127
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3415
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2054
setLastEdit(Revision $revision)
Set the latest revision.
Definition: WikiPage.php:756
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:3346
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4883
$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:1587
static getMainWANInstance()
Get the main WAN cache object.
int $wgCommentTableSchemaMigrationStage
Comment table schema migration stage.
string $mLinksUpdated
Definition: WikiPage.php:96
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:692
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3792
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:467
getParserCache()
Definition: WikiPage.php:238
string $mTouched
Definition: WikiPage.php:91
$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:642
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:3968
int $mId
Definition: WikiPage.php:66
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:777
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:245
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:1997
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:71
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:3574
getTimestamp()
Definition: Revision.php:1008
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3826
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2445
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1018
Title $mTitle
Definition: WikiPage.php:50
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage.
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3478
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:1277
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1089
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:296
clear()
Clear the object.
Definition: WikiPage.php:284
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:3658
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3805
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1305
$source
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:906
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:157
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:1557
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2635
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:1127
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
newDerivedDataUpdater()
Definition: WikiPage.php:1661
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:670
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:823
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:86
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1600
$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:295
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:716
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:101
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3814
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:990
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2462
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:437
getActionOverrides()
Definition: WikiPage.php:255
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:612
Class DeletePageJob.
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:304
getRevisionStore()
Definition: WikiPage.php:224
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:1995
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:630
static onArticleEdit(Title $title, Revision $revision=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3433
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:1429
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:659
$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:3105
$mIsRedirect
Definition: WikiPage.php:56
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:2599
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
getRevision()
Get the latest revision.
Definition: WikiPage.php:765
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
const PRC_PATROLLED
getRevisionRenderer()
Definition: WikiPage.php:231
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:1764
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:2992
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:2537
const FOR_PUBLIC
Definition: Revision.php:55
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:2566
doDeleteUpdates( $id, Content $content=null, Revision $revision=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:2979
Revision $mLastRevision
Definition: WikiPage.php:81
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:1464
$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:1343
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:1078
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1561
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:1001
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:115
$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:61
getCacheTime()
Definition: CacheTime.php:60
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:703
$cache
Definition: mcc.php:33
$params
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3529
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1240
getTitle()
Get the title object of the article.
Definition: WikiPage.php:276
const NS_CATEGORY
Definition: Defines.php:78
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3856
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:1997
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.
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:936
hasViewableContent()
Check if this page is something we&#39;re going to be showing some sort of sensible content for...
Definition: WikiPage.php:603
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object...
Definition: Revision.php:526
$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:974
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:1781
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:861
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:538
const RAW
Definition: Revision.php:57
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:658
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:268
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1263
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2095
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3560
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2380
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:1521
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:1919
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:923
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:165
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:892
getUser( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:836
static newDynamic(Title $title, array $params)
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:615
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:682
wfRandom()
Get a random decimal value between 0 and 1, in a way not likely to give duplicate values for any real...
getUserText( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:874
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:3506
static convertSelectType( $type)
Convert &#39;fromdb&#39;, &#39;fromdbmaster&#39; and &#39;forupdate&#39; to READ_* constants.
Definition: WikiPage.php:207
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:2026
getId()
Get the user&#39;s ID.
Definition: User.php:2437
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4670
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:2137
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3779
const EDIT_NEW
Definition: Defines.php:152
getTimestamp()
Definition: WikiPage.php:809
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:451
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:1708
static hasDifferencesOutsideMainSlot(Revision $a, Revision $b)
Helper method for checking whether two revisions have differences that go beyond the main slot...
Definition: WikiPage.php:1502
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:3712
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3379
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:1543
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:3059
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:1646
$revQuery
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:404
$mDataLoaded
Definition: WikiPage.php:55
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1177
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...
the value of this variable comes from LanguageConverter indexed by page_id indexed by prefixed DB keys on which the links will be shown can modify can modify can modify this should be populated with an alert message to that effect to be fed to an HTMLForm object $context
Definition: hooks.txt:1690
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:971
formatExpiry( $expiry)
Definition: WikiPage.php:2423
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:1043
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1199
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1350
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:2510
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:2951
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:1844
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that 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:785
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:592
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:119
static selectFields()
Return the list of revision fields that should be selected to create a new page.
Definition: WikiPage.php:326
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:315
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:1951
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Title $mRedirectTarget
Definition: WikiPage.php:76
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:107
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page&#39;s history.
Definition: Revision.php:1214
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object...
Definition: WikiPage.php:365
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:2800
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:195
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:512
Special handling for category pages.
static singleton()
Get the signleton instance of this class.
purgeSquid()
Purge all applicable CDN URLs.
Definition: Title.php:3978
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1487
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:2125
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:52
getCreator( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:855
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2487