MediaWiki  master
WikiPage.php
Go to the documentation of this file.
1 <?php
38 
45 class WikiPage implements Page, IDBAccessObject {
46  // Constants for $mDataLoadedFrom and related
47 
51  public $mTitle = null;
52 
57  public $mDataLoaded = false;
58 
63  public $mIsRedirect = false;
64 
69  public $mLatest = false;
70 
72  public $mPreparedEdit = false;
73 
77  protected $mId = null;
78 
82  protected $mDataLoadedFrom = self::READ_NONE;
83 
87  protected $mRedirectTarget = null;
88 
92  protected $mLastRevision = null;
93 
97  protected $mTimestamp = '';
98 
102  protected $mTouched = '19700101000000';
103 
107  protected $mLinksUpdated = '19700101000000';
108 
113 
118  public function __construct( Title $title ) {
119  $this->mTitle = $title;
120  }
121 
126  public function __clone() {
127  $this->mTitle = clone $this->mTitle;
128  }
129 
138  public static function factory( Title $title ) {
139  $ns = $title->getNamespace();
140 
141  if ( $ns == NS_MEDIA ) {
142  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
143  } elseif ( $ns < 0 ) {
144  throw new MWException( "Invalid or virtual namespace $ns given." );
145  }
146 
147  $page = null;
148  if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
149  return $page;
150  }
151 
152  switch ( $ns ) {
153  case NS_FILE:
154  $page = new WikiFilePage( $title );
155  break;
156  case NS_CATEGORY:
157  $page = new WikiCategoryPage( $title );
158  break;
159  default:
160  $page = new WikiPage( $title );
161  }
162 
163  return $page;
164  }
165 
176  public static function newFromID( $id, $from = 'fromdb' ) {
177  // page ids are never 0 or negative, see T63166
178  if ( $id < 1 ) {
179  return null;
180  }
181 
182  $from = self::convertSelectType( $from );
183  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
184  $pageQuery = self::getQueryInfo();
185  $row = $db->selectRow(
186  $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
187  [], $pageQuery['joins']
188  );
189  if ( !$row ) {
190  return null;
191  }
192  return self::newFromRow( $row, $from );
193  }
194 
206  public static function newFromRow( $row, $from = 'fromdb' ) {
207  $page = self::factory( Title::newFromRow( $row ) );
208  $page->loadFromRow( $row, $from );
209  return $page;
210  }
211 
218  protected static function convertSelectType( $type ) {
219  switch ( $type ) {
220  case 'fromdb':
221  return self::READ_NORMAL;
222  case 'fromdbmaster':
223  return self::READ_LATEST;
224  case 'forupdate':
225  return self::READ_LOCKING;
226  default:
227  // It may already be an integer or whatever else
228  return $type;
229  }
230  }
231 
235  private function getRevisionStore() {
236  return MediaWikiServices::getInstance()->getRevisionStore();
237  }
238 
242  private function getRevisionRenderer() {
243  return MediaWikiServices::getInstance()->getRevisionRenderer();
244  }
245 
249  private function getSlotRoleRegistry() {
250  return MediaWikiServices::getInstance()->getSlotRoleRegistry();
251  }
252 
256  private function getParserCache() {
257  return MediaWikiServices::getInstance()->getParserCache();
258  }
259 
263  private function getDBLoadBalancer() {
264  return MediaWikiServices::getInstance()->getDBLoadBalancer();
265  }
266 
273  public function getActionOverrides() {
274  return $this->getContentHandler()->getActionOverrides();
275  }
276 
286  public function getContentHandler() {
288  }
289 
294  public function getTitle() {
295  return $this->mTitle;
296  }
297 
302  public function clear() {
303  $this->mDataLoaded = false;
304  $this->mDataLoadedFrom = self::READ_NONE;
305 
306  $this->clearCacheFields();
307  }
308 
313  protected function clearCacheFields() {
314  $this->mId = null;
315  $this->mRedirectTarget = null; // Title object if set
316  $this->mLastRevision = null; // Latest revision
317  $this->mTouched = '19700101000000';
318  $this->mLinksUpdated = '19700101000000';
319  $this->mTimestamp = '';
320  $this->mIsRedirect = false;
321  $this->mLatest = false;
322  // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
323  // checks the requested rev ID and content against the cached one. For most
324  // content types, the output should not change during the lifetime of this cache.
325  // Clearing it can cause extra parses on edit for no reason.
326  }
327 
333  public function clearPreparedEdit() {
334  $this->mPreparedEdit = false;
335  }
336 
344  public static function selectFields() {
346 
347  wfDeprecated( __METHOD__, '1.31' );
348 
349  $fields = [
350  'page_id',
351  'page_namespace',
352  'page_title',
353  'page_restrictions',
354  'page_is_redirect',
355  'page_is_new',
356  'page_random',
357  'page_touched',
358  'page_links_updated',
359  'page_latest',
360  'page_len',
361  ];
362 
363  if ( $wgContentHandlerUseDB ) {
364  $fields[] = 'page_content_model';
365  }
366 
367  if ( $wgPageLanguageUseDB ) {
368  $fields[] = 'page_lang';
369  }
370 
371  return $fields;
372  }
373 
383  public static function getQueryInfo() {
385 
386  $ret = [
387  'tables' => [ 'page' ],
388  'fields' => [
389  'page_id',
390  'page_namespace',
391  'page_title',
392  'page_restrictions',
393  'page_is_redirect',
394  'page_is_new',
395  'page_random',
396  'page_touched',
397  'page_links_updated',
398  'page_latest',
399  'page_len',
400  ],
401  'joins' => [],
402  ];
403 
404  if ( $wgContentHandlerUseDB ) {
405  $ret['fields'][] = 'page_content_model';
406  }
407 
408  if ( $wgPageLanguageUseDB ) {
409  $ret['fields'][] = 'page_lang';
410  }
411 
412  return $ret;
413  }
414 
422  protected function pageData( $dbr, $conditions, $options = [] ) {
423  $pageQuery = self::getQueryInfo();
424 
425  // Avoid PHP 7.1 warning of passing $this by reference
426  $wikiPage = $this;
427 
428  Hooks::run( 'ArticlePageDataBefore', [
429  &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
430  ] );
431 
432  $row = $dbr->selectRow(
433  $pageQuery['tables'],
434  $pageQuery['fields'],
435  $conditions,
436  __METHOD__,
437  $options,
438  $pageQuery['joins']
439  );
440 
441  Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
442 
443  return $row;
444  }
445 
455  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
456  return $this->pageData( $dbr, [
457  'page_namespace' => $title->getNamespace(),
458  'page_title' => $title->getDBkey() ], $options );
459  }
460 
469  public function pageDataFromId( $dbr, $id, $options = [] ) {
470  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
471  }
472 
485  public function loadPageData( $from = 'fromdb' ) {
486  $from = self::convertSelectType( $from );
487  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
488  // We already have the data from the correct location, no need to load it twice.
489  return;
490  }
491 
492  if ( is_int( $from ) ) {
493  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
494  $loadBalancer = $this->getDBLoadBalancer();
495  $db = $loadBalancer->getConnection( $index );
496  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
497 
498  if ( !$data
499  && $index == DB_REPLICA
500  && $loadBalancer->getServerCount() > 1
501  && $loadBalancer->hasOrMadeRecentMasterChanges()
502  ) {
503  $from = self::READ_LATEST;
504  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
505  $db = $loadBalancer->getConnection( $index );
506  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
507  }
508  } else {
509  // No idea from where the caller got this data, assume replica DB.
510  $data = $from;
511  $from = self::READ_NORMAL;
512  }
513 
514  $this->loadFromRow( $data, $from );
515  }
516 
530  public function wasLoadedFrom( $from ) {
531  $from = self::convertSelectType( $from );
532 
533  if ( !is_int( $from ) ) {
534  // No idea from where the caller got this data, assume replica DB.
535  $from = self::READ_NORMAL;
536  }
537 
538  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
539  return true;
540  }
541 
542  return false;
543  }
544 
556  public function loadFromRow( $data, $from ) {
557  $lc = MediaWikiServices::getInstance()->getLinkCache();
558  $lc->clearLink( $this->mTitle );
559 
560  if ( $data ) {
561  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
562 
563  $this->mTitle->loadFromRow( $data );
564 
565  // Old-fashioned restrictions
566  $this->mTitle->loadRestrictions( $data->page_restrictions );
567 
568  $this->mId = intval( $data->page_id );
569  $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
570  $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
571  $this->mIsRedirect = intval( $data->page_is_redirect );
572  $this->mLatest = intval( $data->page_latest );
573  // T39225: $latest may no longer match the cached latest Revision object.
574  // Double-check the ID of any cached latest Revision object for consistency.
575  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
576  $this->mLastRevision = null;
577  $this->mTimestamp = '';
578  }
579  } else {
580  $lc->addBadLinkObj( $this->mTitle );
581 
582  $this->mTitle->loadFromRow( false );
583 
584  $this->clearCacheFields();
585 
586  $this->mId = 0;
587  }
588 
589  $this->mDataLoaded = true;
590  $this->mDataLoadedFrom = self::convertSelectType( $from );
591  }
592 
596  public function getId() {
597  if ( !$this->mDataLoaded ) {
598  $this->loadPageData();
599  }
600  return $this->mId;
601  }
602 
606  public function exists() {
607  if ( !$this->mDataLoaded ) {
608  $this->loadPageData();
609  }
610  return $this->mId > 0;
611  }
612 
621  public function hasViewableContent() {
622  return $this->mTitle->isKnown();
623  }
624 
630  public function isRedirect() {
631  if ( !$this->mDataLoaded ) {
632  $this->loadPageData();
633  }
634 
635  return (bool)$this->mIsRedirect;
636  }
637 
648  public function getContentModel() {
649  if ( $this->exists() ) {
650  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
651 
652  return $cache->getWithSetCallback(
653  $cache->makeKey( 'page-content-model', $this->getLatest() ),
654  $cache::TTL_MONTH,
655  function () {
656  $rev = $this->getRevision();
657  if ( $rev ) {
658  // Look at the revision's actual content model
659  return $rev->getContentModel();
660  } else {
661  $title = $this->mTitle->getPrefixedDBkey();
662  wfWarn( "Page $title exists but has no (visible) revisions!" );
663  return $this->mTitle->getContentModel();
664  }
665  }
666  );
667  }
668 
669  // use the default model for this page
670  return $this->mTitle->getContentModel();
671  }
672 
677  public function checkTouched() {
678  if ( !$this->mDataLoaded ) {
679  $this->loadPageData();
680  }
681  return ( $this->mId && !$this->mIsRedirect );
682  }
683 
688  public function getTouched() {
689  if ( !$this->mDataLoaded ) {
690  $this->loadPageData();
691  }
692  return $this->mTouched;
693  }
694 
699  public function getLinksTimestamp() {
700  if ( !$this->mDataLoaded ) {
701  $this->loadPageData();
702  }
703  return $this->mLinksUpdated;
704  }
705 
710  public function getLatest() {
711  if ( !$this->mDataLoaded ) {
712  $this->loadPageData();
713  }
714  return (int)$this->mLatest;
715  }
716 
721  public function getOldestRevision() {
722  // Try using the replica DB first, then try the master
723  $rev = $this->mTitle->getFirstRevision();
724  if ( !$rev ) {
725  $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE );
726  }
727  return $rev;
728  }
729 
734  protected function loadLastEdit() {
735  if ( $this->mLastRevision !== null ) {
736  return; // already loaded
737  }
738 
739  $latest = $this->getLatest();
740  if ( !$latest ) {
741  return; // page doesn't exist or is missing page_latest info
742  }
743 
744  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
745  // T39225: if session S1 loads the page row FOR UPDATE, the result always
746  // includes the latest changes committed. This is true even within REPEATABLE-READ
747  // transactions, where S1 normally only sees changes committed before the first S1
748  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
749  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
750  // happened after the first S1 SELECT.
751  // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
752  $flags = Revision::READ_LOCKING;
753  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
754  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
755  // Bug T93976: if page_latest was loaded from the master, fetch the
756  // revision from there as well, as it may not exist yet on a replica DB.
757  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
758  $flags = Revision::READ_LATEST;
759  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
760  } else {
761  $dbr = wfGetDB( DB_REPLICA );
762  $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
763  }
764 
765  if ( $revision ) { // sanity
766  $this->setLastEdit( $revision );
767  }
768  }
769 
774  protected function setLastEdit( Revision $revision ) {
775  $this->mLastRevision = $revision;
776  $this->mTimestamp = $revision->getTimestamp();
777  }
778 
783  public function getRevision() {
784  $this->loadLastEdit();
785  if ( $this->mLastRevision ) {
786  return $this->mLastRevision;
787  }
788  return null;
789  }
790 
795  public function getRevisionRecord() {
796  $this->loadLastEdit();
797  if ( $this->mLastRevision ) {
798  return $this->mLastRevision->getRevisionRecord();
799  }
800  return null;
801  }
802 
816  public function getContent( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
817  $this->loadLastEdit();
818  if ( $this->mLastRevision ) {
819  return $this->mLastRevision->getContent( $audience, $user );
820  }
821  return null;
822  }
823 
827  public function getTimestamp() {
828  // Check if the field has been filled by WikiPage::setTimestamp()
829  if ( !$this->mTimestamp ) {
830  $this->loadLastEdit();
831  }
832 
833  return wfTimestamp( TS_MW, $this->mTimestamp );
834  }
835 
841  public function setTimestamp( $ts ) {
842  $this->mTimestamp = wfTimestamp( TS_MW, $ts );
843  }
844 
854  public function getUser( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
855  $this->loadLastEdit();
856  if ( $this->mLastRevision ) {
857  return $this->mLastRevision->getUser( $audience, $user );
858  } else {
859  return -1;
860  }
861  }
862 
873  public function getCreator( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
874  $revision = $this->getOldestRevision();
875  if ( $revision ) {
876  $userName = $revision->getUserText( $audience, $user );
877  return User::newFromName( $userName, false );
878  } else {
879  return null;
880  }
881  }
882 
892  public function getUserText( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
893  $this->loadLastEdit();
894  if ( $this->mLastRevision ) {
895  return $this->mLastRevision->getUserText( $audience, $user );
896  } else {
897  return '';
898  }
899  }
900 
911  public function getComment( $audience = RevisionRecord::FOR_PUBLIC, User $user = null ) {
912  $this->loadLastEdit();
913  if ( $this->mLastRevision ) {
914  return $this->mLastRevision->getComment( $audience, $user );
915  } else {
916  return '';
917  }
918  }
919 
925  public function getMinorEdit() {
926  $this->loadLastEdit();
927  if ( $this->mLastRevision ) {
928  return $this->mLastRevision->isMinor();
929  } else {
930  return false;
931  }
932  }
933 
942  public function isCountable( $editInfo = false ) {
943  global $wgArticleCountMethod;
944 
945  // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
946 
947  if ( !$this->mTitle->isContentPage() ) {
948  return false;
949  }
950 
951  if ( $editInfo ) {
952  // NOTE: only the main slot can make a page a redirect
953  $content = $editInfo->pstContent;
954  } else {
955  $content = $this->getContent();
956  }
957 
958  if ( !$content || $content->isRedirect() ) {
959  return false;
960  }
961 
962  $hasLinks = null;
963 
964  if ( $wgArticleCountMethod === 'link' ) {
965  // nasty special case to avoid re-parsing to detect links
966 
967  if ( $editInfo ) {
968  // ParserOutput::getLinks() is a 2D array of page links, so
969  // to be really correct we would need to recurse in the array
970  // but the main array should only have items in it if there are
971  // links.
972  $hasLinks = (bool)count( $editInfo->output->getLinks() );
973  } else {
974  // NOTE: keep in sync with RevisionRenderer::getLinkCount
975  // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
976  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
977  [ 'pl_from' => $this->getId() ], __METHOD__ );
978  }
979  }
980 
981  // TODO: MCR: determine $hasLinks for each slot, and use that info
982  // with that slot's Content's isCountable method. That requires per-
983  // slot ParserOutput in the ParserCache, or per-slot info in the
984  // pagelinks table.
985  return $content->isCountable( $hasLinks );
986  }
987 
995  public function getRedirectTarget() {
996  if ( !$this->mTitle->isRedirect() ) {
997  return null;
998  }
999 
1000  if ( $this->mRedirectTarget !== null ) {
1001  return $this->mRedirectTarget;
1002  }
1003 
1004  // Query the redirect table
1005  $dbr = wfGetDB( DB_REPLICA );
1006  $row = $dbr->selectRow( 'redirect',
1007  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1008  [ 'rd_from' => $this->getId() ],
1009  __METHOD__
1010  );
1011 
1012  // rd_fragment and rd_interwiki were added later, populate them if empty
1013  if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
1014  // (T203942) We can't redirect to Media namespace because it's virtual.
1015  // We don't want to modify Title objects farther down the
1016  // line. So, let's fix this here by changing to File namespace.
1017  if ( $row->rd_namespace == NS_MEDIA ) {
1018  $namespace = NS_FILE;
1019  } else {
1020  $namespace = $row->rd_namespace;
1021  }
1022  $this->mRedirectTarget = Title::makeTitle(
1023  $namespace, $row->rd_title,
1024  $row->rd_fragment, $row->rd_interwiki
1025  );
1026  return $this->mRedirectTarget;
1027  }
1028 
1029  // This page doesn't have an entry in the redirect table
1030  $this->mRedirectTarget = $this->insertRedirect();
1031  return $this->mRedirectTarget;
1032  }
1033 
1042  public function insertRedirect() {
1043  $content = $this->getContent();
1044  $retval = $content ? $content->getUltimateRedirectTarget() : null;
1045  if ( !$retval ) {
1046  return null;
1047  }
1048 
1049  // Update the DB post-send if the page has not cached since now
1050  $latest = $this->getLatest();
1052  function () use ( $retval, $latest ) {
1053  $this->insertRedirectEntry( $retval, $latest );
1054  },
1056  wfGetDB( DB_MASTER )
1057  );
1058 
1059  return $retval;
1060  }
1061 
1067  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1068  $dbw = wfGetDB( DB_MASTER );
1069  $dbw->startAtomic( __METHOD__ );
1070 
1071  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1072  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1073  $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1074  $dbw->upsert(
1075  'redirect',
1076  [
1077  'rd_from' => $this->getId(),
1078  'rd_namespace' => $rt->getNamespace(),
1079  'rd_title' => $rt->getDBkey(),
1080  'rd_fragment' => $truncatedFragment,
1081  'rd_interwiki' => $rt->getInterwiki(),
1082  ],
1083  [ 'rd_from' ],
1084  [
1085  'rd_namespace' => $rt->getNamespace(),
1086  'rd_title' => $rt->getDBkey(),
1087  'rd_fragment' => $truncatedFragment,
1088  'rd_interwiki' => $rt->getInterwiki(),
1089  ],
1090  __METHOD__
1091  );
1092  }
1093 
1094  $dbw->endAtomic( __METHOD__ );
1095  }
1096 
1102  public function followRedirect() {
1103  return $this->getRedirectURL( $this->getRedirectTarget() );
1104  }
1105 
1113  public function getRedirectURL( $rt ) {
1114  if ( !$rt ) {
1115  return false;
1116  }
1117 
1118  if ( $rt->isExternal() ) {
1119  if ( $rt->isLocal() ) {
1120  // Offsite wikis need an HTTP redirect.
1121  // This can be hard to reverse and may produce loops,
1122  // so they may be disabled in the site configuration.
1123  $source = $this->mTitle->getFullURL( 'redirect=no' );
1124  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1125  } else {
1126  // External pages without "local" bit set are not valid
1127  // redirect targets
1128  return false;
1129  }
1130  }
1131 
1132  if ( $rt->isSpecialPage() ) {
1133  // Gotta handle redirects to special pages differently:
1134  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1135  // Some pages are not valid targets.
1136  if ( $rt->isValidRedirectTarget() ) {
1137  return $rt->getFullURL();
1138  } else {
1139  return false;
1140  }
1141  }
1142 
1143  return $rt;
1144  }
1145 
1151  public function getContributors() {
1152  // @todo: This is expensive; cache this info somewhere.
1153 
1154  $dbr = wfGetDB( DB_REPLICA );
1155 
1156  $actorMigration = ActorMigration::newMigration();
1157  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1158 
1159  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1160 
1161  $fields = [
1162  'user_id' => $actorQuery['fields']['rev_user'],
1163  'user_name' => $actorQuery['fields']['rev_user_text'],
1164  'actor_id' => $actorQuery['fields']['rev_actor'],
1165  'user_real_name' => 'MIN(user_real_name)',
1166  'timestamp' => 'MAX(rev_timestamp)',
1167  ];
1168 
1169  $conds = [ 'rev_page' => $this->getId() ];
1170 
1171  // The user who made the top revision gets credited as "this page was last edited by
1172  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1173  $user = $this->getUser()
1174  ? User::newFromId( $this->getUser() )
1175  : User::newFromName( $this->getUserText(), false );
1176  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1177 
1178  // Username hidden?
1179  $conds[] = "{$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER )} = 0";
1180 
1181  $jconds = [
1182  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1183  ] + $actorQuery['joins'];
1184 
1185  $options = [
1186  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1187  'ORDER BY' => 'timestamp DESC',
1188  ];
1189 
1190  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1191  return new UserArrayFromResult( $res );
1192  }
1193 
1201  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1202  return $parserOptions->getStubThreshold() == 0
1203  && $this->exists()
1204  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1205  && $this->getContentHandler()->isParserCacheSupported();
1206  }
1207 
1223  public function getParserOutput(
1224  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1225  ) {
1226  $useParserCache =
1227  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1228 
1229  if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1230  throw new InvalidArgumentException(
1231  'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1232  );
1233  }
1234 
1235  wfDebug( __METHOD__ .
1236  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1237  if ( $parserOptions->getStubThreshold() ) {
1238  wfIncrStats( 'pcache.miss.stub' );
1239  }
1240 
1241  if ( $useParserCache ) {
1242  $parserOutput = $this->getParserCache()
1243  ->get( $this, $parserOptions );
1244  if ( $parserOutput !== false ) {
1245  return $parserOutput;
1246  }
1247  }
1248 
1249  if ( $oldid === null || $oldid === 0 ) {
1250  $oldid = $this->getLatest();
1251  }
1252 
1253  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1254  $pool->execute();
1255 
1256  return $pool->getParserOutput();
1257  }
1258 
1264  public function doViewUpdates( User $user, $oldid = 0 ) {
1265  if ( wfReadOnly() ) {
1266  return;
1267  }
1268 
1269  // Update newtalk / watchlist notification status;
1270  // Avoid outage if the master is not reachable by using a deferred updated
1272  function () use ( $user, $oldid ) {
1273  Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1274 
1275  $user->clearNotification( $this->mTitle, $oldid );
1276  },
1278  );
1279  }
1280 
1287  public function doPurge() {
1288  // Avoid PHP 7.1 warning of passing $this by reference
1289  $wikiPage = $this;
1290 
1291  if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1292  return false;
1293  }
1294 
1295  $this->mTitle->invalidateCache();
1296 
1297  // Clear file cache
1299  // Send purge after above page_touched update was committed
1301  new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1303  );
1304 
1305  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1306  $messageCache = MessageCache::singleton();
1307  $messageCache->updateMessageOverride( $this->mTitle, $this->getContent() );
1308  }
1309 
1310  return true;
1311  }
1312 
1329  public function insertOn( $dbw, $pageId = null ) {
1330  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1331  $dbw->insert(
1332  'page',
1333  [
1334  'page_namespace' => $this->mTitle->getNamespace(),
1335  'page_title' => $this->mTitle->getDBkey(),
1336  'page_restrictions' => '',
1337  'page_is_redirect' => 0, // Will set this shortly...
1338  'page_is_new' => 1,
1339  'page_random' => wfRandom(),
1340  'page_touched' => $dbw->timestamp(),
1341  'page_latest' => 0, // Fill this in shortly...
1342  'page_len' => 0, // Fill this in shortly...
1343  ] + $pageIdForInsert,
1344  __METHOD__,
1345  [ 'IGNORE' ]
1346  );
1347 
1348  if ( $dbw->affectedRows() > 0 ) {
1349  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1350  $this->mId = $newid;
1351  $this->mTitle->resetArticleID( $newid );
1352 
1353  return $newid;
1354  } else {
1355  return false; // nothing changed
1356  }
1357  }
1358 
1374  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1375  $lastRevIsRedirect = null
1376  ) {
1377  global $wgContentHandlerUseDB;
1378 
1379  // TODO: move into PageUpdater or PageStore
1380  // NOTE: when doing that, make sure cached fields get reset in doEditContent,
1381  // and in the compat stub!
1382 
1383  // Assertion to try to catch T92046
1384  if ( (int)$revision->getId() === 0 ) {
1385  throw new InvalidArgumentException(
1386  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1387  );
1388  }
1389 
1390  $content = $revision->getContent();
1391  $len = $content ? $content->getSize() : 0;
1392  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1393 
1394  $conditions = [ 'page_id' => $this->getId() ];
1395 
1396  if ( !is_null( $lastRevision ) ) {
1397  // An extra check against threads stepping on each other
1398  $conditions['page_latest'] = $lastRevision;
1399  }
1400 
1401  $revId = $revision->getId();
1402  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1403 
1404  $row = [ /* SET */
1405  'page_latest' => $revId,
1406  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1407  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1408  'page_is_redirect' => $rt !== null ? 1 : 0,
1409  'page_len' => $len,
1410  ];
1411 
1412  if ( $wgContentHandlerUseDB ) {
1413  $row['page_content_model'] = $revision->getContentModel();
1414  }
1415 
1416  $dbw->update( 'page',
1417  $row,
1418  $conditions,
1419  __METHOD__ );
1420 
1421  $result = $dbw->affectedRows() > 0;
1422  if ( $result ) {
1423  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1424  $this->setLastEdit( $revision );
1425  $this->mLatest = $revision->getId();
1426  $this->mIsRedirect = (bool)$rt;
1427  // Update the LinkCache.
1428  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1429  $linkCache->addGoodLinkObj(
1430  $this->getId(),
1431  $this->mTitle,
1432  $len,
1433  $this->mIsRedirect,
1434  $this->mLatest,
1435  $revision->getContentModel()
1436  );
1437  }
1438 
1439  return $result;
1440  }
1441 
1453  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1454  // Always update redirects (target link might have changed)
1455  // Update/Insert if we don't know if the last revision was a redirect or not
1456  // Delete if changing from redirect to non-redirect
1457  $isRedirect = !is_null( $redirectTitle );
1458 
1459  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1460  return true;
1461  }
1462 
1463  if ( $isRedirect ) {
1464  $this->insertRedirectEntry( $redirectTitle );
1465  } else {
1466  // This is not a redirect, remove row from redirect table
1467  $where = [ 'rd_from' => $this->getId() ];
1468  $dbw->delete( 'redirect', $where, __METHOD__ );
1469  }
1470 
1471  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1472  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1473  }
1474 
1475  return ( $dbw->affectedRows() != 0 );
1476  }
1477 
1488  public function updateIfNewerOn( $dbw, $revision ) {
1489  $row = $dbw->selectRow(
1490  [ 'revision', 'page' ],
1491  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1492  [
1493  'page_id' => $this->getId(),
1494  'page_latest=rev_id' ],
1495  __METHOD__ );
1496 
1497  if ( $row ) {
1498  if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1499  return false;
1500  }
1501  $prev = $row->rev_id;
1502  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1503  } else {
1504  // No or missing previous revision; mark the page as new
1505  $prev = 0;
1506  $lastRevIsRedirect = null;
1507  }
1508 
1509  $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1510 
1511  return $ret;
1512  }
1513 
1526  public static function hasDifferencesOutsideMainSlot( Revision $a, Revision $b ) {
1527  $aSlots = $a->getRevisionRecord()->getSlots();
1528  $bSlots = $b->getRevisionRecord()->getSlots();
1529  $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1530 
1531  return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1532  }
1533 
1545  public function getUndoContent( Revision $undo, Revision $undoafter ) {
1546  // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1547 
1548  if ( self::hasDifferencesOutsideMainSlot( $undo, $undoafter ) ) {
1549  // Cannot yet undo edits that involve anything other the main slot.
1550  return false;
1551  }
1552 
1553  $handler = $undo->getContentHandler();
1554  return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1555  }
1556 
1567  public function supportsSections() {
1568  return $this->getContentHandler()->supportsSections();
1569  }
1570 
1585  public function replaceSectionContent(
1586  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1587  ) {
1588  $baseRevId = null;
1589  if ( $edittime && $sectionId !== 'new' ) {
1590  $lb = $this->getDBLoadBalancer();
1591  $dbr = $lb->getConnectionRef( DB_REPLICA );
1592  $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1593  // Try the master if this thread may have just added it.
1594  // This could be abstracted into a Revision method, but we don't want
1595  // to encourage loading of revisions by timestamp.
1596  if ( !$rev
1597  && $lb->getServerCount() > 1
1598  && $lb->hasOrMadeRecentMasterChanges()
1599  ) {
1600  $dbw = $lb->getConnectionRef( DB_MASTER );
1601  $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1602  }
1603  if ( $rev ) {
1604  $baseRevId = $rev->getId();
1605  }
1606  }
1607 
1608  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1609  }
1610 
1624  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1625  $sectionTitle = '', $baseRevId = null
1626  ) {
1627  if ( strval( $sectionId ) === '' ) {
1628  // Whole-page edit; let the whole text through
1629  $newContent = $sectionContent;
1630  } else {
1631  if ( !$this->supportsSections() ) {
1632  throw new MWException( "sections not supported for content model " .
1633  $this->getContentHandler()->getModelID() );
1634  }
1635 
1636  // T32711: always use current version when adding a new section
1637  if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1638  $oldContent = $this->getContent();
1639  } else {
1640  $rev = Revision::newFromId( $baseRevId );
1641  if ( !$rev ) {
1642  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1643  $this->getId() . "; section: $sectionId)\n" );
1644  return null;
1645  }
1646 
1647  $oldContent = $rev->getContent();
1648  }
1649 
1650  if ( !$oldContent ) {
1651  wfDebug( __METHOD__ . ": no page text\n" );
1652  return null;
1653  }
1654 
1655  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1656  }
1657 
1658  return $newContent;
1659  }
1660 
1670  public function checkFlags( $flags ) {
1671  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1672  if ( $this->exists() ) {
1673  $flags |= EDIT_UPDATE;
1674  } else {
1675  $flags |= EDIT_NEW;
1676  }
1677  }
1678 
1679  return $flags;
1680  }
1681 
1685  private function newDerivedDataUpdater() {
1687 
1689  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1690  $this->getRevisionStore(),
1691  $this->getRevisionRenderer(),
1692  $this->getSlotRoleRegistry(),
1693  $this->getParserCache(),
1696  MediaWikiServices::getInstance()->getContentLanguage(),
1697  MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
1698  );
1699 
1700  $derivedDataUpdater->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
1701  $derivedDataUpdater->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership );
1702  $derivedDataUpdater->setArticleCountMethod( $wgArticleCountMethod );
1703 
1704  return $derivedDataUpdater;
1705  }
1706 
1734  private function getDerivedDataUpdater(
1735  User $forUser = null,
1736  RevisionRecord $forRevision = null,
1737  RevisionSlotsUpdate $forUpdate = null,
1738  $forEdit = false
1739  ) {
1740  if ( !$forRevision && !$forUpdate ) {
1741  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1742  // going to use it with.
1743  $this->derivedDataUpdater = null;
1744  }
1745 
1746  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1747  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1748  // to it did not yet initialize it, because we don't know what data it will be
1749  // initialized with.
1750  $this->derivedDataUpdater = null;
1751  }
1752 
1753  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1754  // However, there is no good way to construct a cache key. We'd need to check against all
1755  // cached instances.
1756 
1757  if ( $this->derivedDataUpdater
1758  && !$this->derivedDataUpdater->isReusableFor(
1759  $forUser,
1760  $forRevision,
1761  $forUpdate,
1762  $forEdit ? $this->getLatest() : null
1763  )
1764  ) {
1765  $this->derivedDataUpdater = null;
1766  }
1767 
1768  if ( !$this->derivedDataUpdater ) {
1769  $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1770  }
1771 
1773  }
1774 
1790  public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1792 
1793  $pageUpdater = new PageUpdater(
1794  $user,
1795  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1796  $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1797  $this->getDBLoadBalancer(),
1798  $this->getRevisionStore(),
1799  $this->getSlotRoleRegistry()
1800  );
1801 
1802  $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
1803  $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
1804  $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
1805 
1806  return $pageUpdater;
1807  }
1808 
1871  public function doEditContent(
1872  Content $content, $summary, $flags = 0, $originalRevId = false,
1873  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1874  ) {
1875  global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1876 
1877  if ( !( $summary instanceof CommentStoreComment ) ) {
1878  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1879  }
1880 
1881  if ( !$user ) {
1882  $user = $wgUser;
1883  }
1884 
1885  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1886  // Checking the minoredit right should be done in the same place the 'bot' right is
1887  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1888  if ( ( $flags & EDIT_MINOR ) && !$user->isAllowed( 'minoredit' ) ) {
1889  $flags = ( $flags & ~EDIT_MINOR );
1890  }
1891 
1892  $slotsUpdate = new RevisionSlotsUpdate();
1893  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1894 
1895  // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1896  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1897  // used by this PageUpdater. However, there is no guarantee for this.
1898  $updater = $this->newPageUpdater( $user, $slotsUpdate );
1899  $updater->setContent( SlotRecord::MAIN, $content );
1900  $updater->setOriginalRevisionId( $originalRevId );
1901  $updater->setUndidRevisionId( $undidRevId );
1902 
1903  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1904 
1905  // TODO: this logic should not be in the storage layer, it's here for compatibility
1906  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1907  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1908  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1909 
1910  if ( $needsPatrol && $permissionManager->userCan(
1911  'autopatrol', $user, $this->getTitle()
1912  ) ) {
1913  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1914  }
1915 
1916  $updater->addTags( $tags );
1917 
1918  $revRec = $updater->saveRevision(
1919  $summary,
1920  $flags
1921  );
1922 
1923  // $revRec will be null if the edit failed, or if no new revision was created because
1924  // the content did not change.
1925  if ( $revRec ) {
1926  // update cached fields
1927  // TODO: this is currently redundant to what is done in updateRevisionOn.
1928  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1929  $this->setLastEdit( new Revision( $revRec ) ); // TODO: use RevisionRecord
1930  $this->mLatest = $revRec->getId();
1931  }
1932 
1933  return $updater->getStatus();
1934  }
1935 
1950  public function makeParserOptions( $context ) {
1952 
1953  if ( $this->getTitle()->isConversionTable() ) {
1954  // @todo ConversionTable should become a separate content model, so
1955  // we don't need special cases like this one.
1956  $options->disableContentConversion();
1957  }
1958 
1959  return $options;
1960  }
1961 
1980  public function prepareContentForEdit(
1981  Content $content,
1982  $revision = null,
1983  User $user = null,
1984  $serialFormat = null,
1985  $useCache = true
1986  ) {
1987  global $wgUser;
1988 
1989  if ( !$user ) {
1990  $user = $wgUser;
1991  }
1992 
1993  if ( $revision !== null ) {
1994  if ( $revision instanceof Revision ) {
1995  $revision = $revision->getRevisionRecord();
1996  } elseif ( !( $revision instanceof RevisionRecord ) ) {
1997  throw new InvalidArgumentException(
1998  __METHOD__ . ': invalid $revision argument type ' . gettype( $revision ) );
1999  }
2000  }
2001 
2002  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2003  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2004 
2005  if ( !$updater->isUpdatePrepared() ) {
2006  $updater->prepareContent( $user, $slots, $useCache );
2007 
2008  if ( $revision ) {
2009  $updater->prepareUpdate(
2010  $revision,
2011  [
2012  'causeAction' => 'prepare-edit',
2013  'causeAgent' => $user->getName(),
2014  ]
2015  );
2016  }
2017  }
2018 
2019  return $updater->getPreparedEdit();
2020  }
2021 
2049  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2050  $options += [
2051  'causeAction' => 'edit-page',
2052  'causeAgent' => $user->getName(),
2053  ];
2054 
2055  $revision = $revision->getRevisionRecord();
2056 
2057  $updater = $this->getDerivedDataUpdater( $user, $revision );
2058 
2059  $updater->prepareUpdate( $revision, $options );
2060 
2061  $updater->doUpdates();
2062  }
2063 
2077  public function updateParserCache( array $options = [] ) {
2078  $revision = $this->getRevisionRecord();
2079  if ( !$revision || !$revision->getId() ) {
2080  LoggerFactory::getInstance( 'wikipage' )->info(
2081  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2082  );
2083  return;
2084  }
2085  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2086 
2087  $updater = $this->getDerivedDataUpdater( $user, $revision );
2088  $updater->prepareUpdate( $revision, $options );
2089  $updater->doParserCacheUpdate();
2090  }
2091 
2121  public function doSecondaryDataUpdates( array $options = [] ) {
2122  $options['recursive'] = $options['recursive'] ?? true;
2123  $revision = $this->getRevisionRecord();
2124  if ( !$revision || !$revision->getId() ) {
2125  LoggerFactory::getInstance( 'wikipage' )->info(
2126  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2127  );
2128  return;
2129  }
2130  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2131 
2132  $updater = $this->getDerivedDataUpdater( $user, $revision );
2133  $updater->prepareUpdate( $revision, $options );
2134  $updater->doSecondaryDataUpdates( $options );
2135  }
2136 
2151  public function doUpdateRestrictions( array $limit, array $expiry,
2152  &$cascade, $reason, User $user, $tags = null
2153  ) {
2155 
2156  if ( wfReadOnly() ) {
2157  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2158  }
2159 
2160  $this->loadPageData( 'fromdbmaster' );
2161  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2162  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2163  $id = $this->getId();
2164 
2165  if ( !$cascade ) {
2166  $cascade = false;
2167  }
2168 
2169  // Take this opportunity to purge out expired restrictions
2171 
2172  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2173  // we expect a single selection, but the schema allows otherwise.
2174  $isProtected = false;
2175  $protect = false;
2176  $changed = false;
2177 
2178  $dbw = wfGetDB( DB_MASTER );
2179 
2180  foreach ( $restrictionTypes as $action ) {
2181  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2182  $expiry[$action] = 'infinity';
2183  }
2184  if ( !isset( $limit[$action] ) ) {
2185  $limit[$action] = '';
2186  } elseif ( $limit[$action] != '' ) {
2187  $protect = true;
2188  }
2189 
2190  // Get current restrictions on $action
2191  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2192  if ( $current != '' ) {
2193  $isProtected = true;
2194  }
2195 
2196  if ( $limit[$action] != $current ) {
2197  $changed = true;
2198  } elseif ( $limit[$action] != '' ) {
2199  // Only check expiry change if the action is actually being
2200  // protected, since expiry does nothing on an not-protected
2201  // action.
2202  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2203  $changed = true;
2204  }
2205  }
2206  }
2207 
2208  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2209  $changed = true;
2210  }
2211 
2212  // If nothing has changed, do nothing
2213  if ( !$changed ) {
2214  return Status::newGood();
2215  }
2216 
2217  if ( !$protect ) { // No protection at all means unprotection
2218  $revCommentMsg = 'unprotectedarticle-comment';
2219  $logAction = 'unprotect';
2220  } elseif ( $isProtected ) {
2221  $revCommentMsg = 'modifiedarticleprotection-comment';
2222  $logAction = 'modify';
2223  } else {
2224  $revCommentMsg = 'protectedarticle-comment';
2225  $logAction = 'protect';
2226  }
2227 
2228  $logRelationsValues = [];
2229  $logRelationsField = null;
2230  $logParamsDetails = [];
2231 
2232  // Null revision (used for change tag insertion)
2233  $nullRevision = null;
2234 
2235  if ( $id ) { // Protection of existing page
2236  // Avoid PHP 7.1 warning of passing $this by reference
2237  $wikiPage = $this;
2238 
2239  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2240  return Status::newGood();
2241  }
2242 
2243  // Only certain restrictions can cascade...
2244  $editrestriction = isset( $limit['edit'] )
2245  ? [ $limit['edit'] ]
2246  : $this->mTitle->getRestrictions( 'edit' );
2247  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2248  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2249  }
2250  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2251  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2252  }
2253 
2254  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2255  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2256  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2257  }
2258  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2259  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2260  }
2261 
2262  // The schema allows multiple restrictions
2263  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2264  $cascade = false;
2265  }
2266 
2267  // insert null revision to identify the page protection change as edit summary
2268  $latest = $this->getLatest();
2269  $nullRevision = $this->insertProtectNullRevision(
2270  $revCommentMsg,
2271  $limit,
2272  $expiry,
2273  $cascade,
2274  $reason,
2275  $user
2276  );
2277 
2278  if ( $nullRevision === null ) {
2279  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2280  }
2281 
2282  $logRelationsField = 'pr_id';
2283 
2284  // Update restrictions table
2285  foreach ( $limit as $action => $restrictions ) {
2286  $dbw->delete(
2287  'page_restrictions',
2288  [
2289  'pr_page' => $id,
2290  'pr_type' => $action
2291  ],
2292  __METHOD__
2293  );
2294  if ( $restrictions != '' ) {
2295  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2296  $dbw->insert(
2297  'page_restrictions',
2298  [
2299  'pr_page' => $id,
2300  'pr_type' => $action,
2301  'pr_level' => $restrictions,
2302  'pr_cascade' => $cascadeValue,
2303  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2304  ],
2305  __METHOD__
2306  );
2307  $logRelationsValues[] = $dbw->insertId();
2308  $logParamsDetails[] = [
2309  'type' => $action,
2310  'level' => $restrictions,
2311  'expiry' => $expiry[$action],
2312  'cascade' => (bool)$cascadeValue,
2313  ];
2314  }
2315  }
2316 
2317  // Clear out legacy restriction fields
2318  $dbw->update(
2319  'page',
2320  [ 'page_restrictions' => '' ],
2321  [ 'page_id' => $id ],
2322  __METHOD__
2323  );
2324 
2325  // Avoid PHP 7.1 warning of passing $this by reference
2326  $wikiPage = $this;
2327 
2328  Hooks::run( 'NewRevisionFromEditComplete',
2329  [ $this, $nullRevision, $latest, $user ] );
2330  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2331  } else { // Protection of non-existing page (also known as "title protection")
2332  // Cascade protection is meaningless in this case
2333  $cascade = false;
2334 
2335  if ( $limit['create'] != '' ) {
2336  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2337  $dbw->replace( 'protected_titles',
2338  [ [ 'pt_namespace', 'pt_title' ] ],
2339  [
2340  'pt_namespace' => $this->mTitle->getNamespace(),
2341  'pt_title' => $this->mTitle->getDBkey(),
2342  'pt_create_perm' => $limit['create'],
2343  'pt_timestamp' => $dbw->timestamp(),
2344  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2345  'pt_user' => $user->getId(),
2346  ] + $commentFields, __METHOD__
2347  );
2348  $logParamsDetails[] = [
2349  'type' => 'create',
2350  'level' => $limit['create'],
2351  'expiry' => $expiry['create'],
2352  ];
2353  } else {
2354  $dbw->delete( 'protected_titles',
2355  [
2356  'pt_namespace' => $this->mTitle->getNamespace(),
2357  'pt_title' => $this->mTitle->getDBkey()
2358  ], __METHOD__
2359  );
2360  }
2361  }
2362 
2363  $this->mTitle->flushRestrictions();
2364  InfoAction::invalidateCache( $this->mTitle );
2365 
2366  if ( $logAction == 'unprotect' ) {
2367  $params = [];
2368  } else {
2369  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2370  $params = [
2371  '4::description' => $protectDescriptionLog, // parameter for IRC
2372  '5:bool:cascade' => $cascade,
2373  'details' => $logParamsDetails, // parameter for localize and api
2374  ];
2375  }
2376 
2377  // Update the protection log
2378  $logEntry = new ManualLogEntry( 'protect', $logAction );
2379  $logEntry->setTarget( $this->mTitle );
2380  $logEntry->setComment( $reason );
2381  $logEntry->setPerformer( $user );
2382  $logEntry->setParameters( $params );
2383  if ( !is_null( $nullRevision ) ) {
2384  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2385  }
2386  $logEntry->setTags( $tags );
2387  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2388  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2389  }
2390  $logId = $logEntry->insert();
2391  $logEntry->publish( $logId );
2392 
2393  return Status::newGood( $logId );
2394  }
2395 
2407  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2408  array $expiry, $cascade, $reason, $user = null
2409  ) {
2410  $dbw = wfGetDB( DB_MASTER );
2411 
2412  // Prepare a null revision to be added to the history
2413  $editComment = wfMessage(
2414  $revCommentMsg,
2415  $this->mTitle->getPrefixedText(),
2416  $user ? $user->getName() : ''
2417  )->inContentLanguage()->text();
2418  if ( $reason ) {
2419  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2420  }
2421  $protectDescription = $this->protectDescription( $limit, $expiry );
2422  if ( $protectDescription ) {
2423  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2424  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2425  ->inContentLanguage()->text();
2426  }
2427  if ( $cascade ) {
2428  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2429  $editComment .= wfMessage( 'brackets' )->params(
2430  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2431  )->inContentLanguage()->text();
2432  }
2433 
2434  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2435  if ( $nullRev ) {
2436  $nullRev->insertOn( $dbw );
2437 
2438  // Update page record and touch page
2439  $oldLatest = $nullRev->getParentId();
2440  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2441  }
2442 
2443  return $nullRev;
2444  }
2445 
2450  protected function formatExpiry( $expiry ) {
2451  if ( $expiry != 'infinity' ) {
2452  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2453  return wfMessage(
2454  'protect-expiring',
2455  $contLang->timeanddate( $expiry, false, false ),
2456  $contLang->date( $expiry, false, false ),
2457  $contLang->time( $expiry, false, false )
2458  )->inContentLanguage()->text();
2459  } else {
2460  return wfMessage( 'protect-expiry-indefinite' )
2461  ->inContentLanguage()->text();
2462  }
2463  }
2464 
2472  public function protectDescription( array $limit, array $expiry ) {
2473  $protectDescription = '';
2474 
2475  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2476  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2477  # All possible message keys are listed here for easier grepping:
2478  # * restriction-create
2479  # * restriction-edit
2480  # * restriction-move
2481  # * restriction-upload
2482  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2483  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2484  # with '' filtered out. All possible message keys are listed below:
2485  # * protect-level-autoconfirmed
2486  # * protect-level-sysop
2487  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2488  ->inContentLanguage()->text();
2489 
2490  $expiryText = $this->formatExpiry( $expiry[$action] );
2491 
2492  if ( $protectDescription !== '' ) {
2493  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2494  }
2495  $protectDescription .= wfMessage( 'protect-summary-desc' )
2496  ->params( $actionText, $restrictionsText, $expiryText )
2497  ->inContentLanguage()->text();
2498  }
2499 
2500  return $protectDescription;
2501  }
2502 
2514  public function protectDescriptionLog( array $limit, array $expiry ) {
2515  $protectDescriptionLog = '';
2516 
2517  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2518  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2519  $expiryText = $this->formatExpiry( $expiry[$action] );
2520  $protectDescriptionLog .=
2521  $dirMark .
2522  "[$action=$restrictions] ($expiryText)";
2523  }
2524 
2525  return trim( $protectDescriptionLog );
2526  }
2527 
2537  protected static function flattenRestrictions( $limit ) {
2538  if ( !is_array( $limit ) ) {
2539  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2540  }
2541 
2542  $bits = [];
2543  ksort( $limit );
2544 
2545  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2546  $bits[] = "$action=$restrictions";
2547  }
2548 
2549  return implode( ':', $bits );
2550  }
2551 
2564  public function isBatchedDelete( $safetyMargin = 0 ) {
2566 
2567  $dbr = wfGetDB( DB_REPLICA );
2568  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2569  $revCount += $safetyMargin;
2570 
2571  return $revCount >= $wgDeleteRevisionsBatchSize;
2572  }
2573 
2593  public function doDeleteArticle(
2594  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2595  $immediate = false
2596  ) {
2597  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2598  [], 'delete', $immediate );
2599 
2600  // Returns true if the page was actually deleted, or is scheduled for deletion
2601  return $status->isOK();
2602  }
2603 
2626  public function doDeleteArticleReal(
2627  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2628  $tags = [], $logsubtype = 'delete', $immediate = false
2629  ) {
2630  global $wgUser;
2631 
2632  wfDebug( __METHOD__ . "\n" );
2633 
2635 
2636  // Avoid PHP 7.1 warning of passing $this by reference
2637  $wikiPage = $this;
2638 
2639  if ( !$deleter ) {
2640  $deleter = $wgUser;
2641  }
2642  if ( !Hooks::run( 'ArticleDelete',
2643  [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2644  ) ) {
2645  if ( $status->isOK() ) {
2646  // Hook aborted but didn't set a fatal status
2647  $status->fatal( 'delete-hook-aborted' );
2648  }
2649  return $status;
2650  }
2651 
2652  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2653  $logsubtype, $immediate );
2654  }
2655 
2664  public function doDeleteArticleBatched(
2665  $reason, $suppress, User $deleter, $tags,
2666  $logsubtype, $immediate = false, $webRequestId = null
2667  ) {
2668  wfDebug( __METHOD__ . "\n" );
2669 
2671 
2672  $dbw = wfGetDB( DB_MASTER );
2673  $dbw->startAtomic( __METHOD__ );
2674 
2675  $this->loadPageData( self::READ_LATEST );
2676  $id = $this->getId();
2677  // T98706: lock the page from various other updates but avoid using
2678  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2679  // the revisions queries (which also JOIN on user). Only lock the page
2680  // row and CAS check on page_latest to see if the trx snapshot matches.
2681  $lockedLatest = $this->lockAndGetLatest();
2682  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2683  $dbw->endAtomic( __METHOD__ );
2684  // Page not there or trx snapshot is stale
2685  $status->error( 'cannotdelete',
2686  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2687  return $status;
2688  }
2689 
2690  // At this point we are now committed to returning an OK
2691  // status unless some DB query error or other exception comes up.
2692  // This way callers don't have to call rollback() if $status is bad
2693  // unless they actually try to catch exceptions (which is rare).
2694 
2695  // we need to remember the old content so we can use it to generate all deletion updates.
2696  $revision = $this->getRevision();
2697  try {
2698  $content = $this->getContent( RevisionRecord::RAW );
2699  } catch ( Exception $ex ) {
2700  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2701  . $ex->getMessage() );
2702 
2703  $content = null;
2704  }
2705 
2706  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2707  // one batch of revisions and defer archival of any others to the job queue.
2708  $explictTrxLogged = false;
2709  while ( true ) {
2710  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2711  if ( $done || !$immediate ) {
2712  break;
2713  }
2714  $dbw->endAtomic( __METHOD__ );
2715  if ( $dbw->explicitTrxActive() ) {
2716  // Explict transactions may never happen here in practice. Log to be sure.
2717  if ( !$explictTrxLogged ) {
2718  $explictTrxLogged = true;
2719  LoggerFactory::getInstance( 'wfDebug' )->debug(
2720  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2721  'title' => $this->getTitle()->getText(),
2722  ] );
2723  }
2724  continue;
2725  }
2726  if ( $dbw->trxLevel() ) {
2727  $dbw->commit();
2728  }
2729  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2730  $lbFactory->waitForReplication();
2731  $dbw->startAtomic( __METHOD__ );
2732  }
2733 
2734  // If done archiving, also delete the article.
2735  if ( !$done ) {
2736  $dbw->endAtomic( __METHOD__ );
2737 
2738  $jobParams = [
2739  'namespace' => $this->getTitle()->getNamespace(),
2740  'title' => $this->getTitle()->getDBkey(),
2741  'wikiPageId' => $id,
2742  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2743  'reason' => $reason,
2744  'suppress' => $suppress,
2745  'userId' => $deleter->getId(),
2746  'tags' => json_encode( $tags ),
2747  'logsubtype' => $logsubtype,
2748  ];
2749 
2750  $job = new DeletePageJob( $jobParams );
2751  JobQueueGroup::singleton()->push( $job );
2752 
2753  $status->warning( 'delete-scheduled',
2754  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2755  } else {
2756  // Get archivedRevisionCount by db query, because there's no better alternative.
2757  // Jobs cannot pass a count of archived revisions to the next job, because additional
2758  // deletion operations can be started while the first is running. Jobs from each
2759  // gracefully interleave, but would not know about each other's count. Deduplication
2760  // in the job queue to avoid simultaneous deletion operations would add overhead.
2761  // Number of archived revisions cannot be known beforehand, because edits can be made
2762  // while deletion operations are being processed, changing the number of archivals.
2763  $archivedRevisionCount = (int)$dbw->selectField(
2764  'archive', 'COUNT(*)',
2765  [
2766  'ar_namespace' => $this->getTitle()->getNamespace(),
2767  'ar_title' => $this->getTitle()->getDBkey(),
2768  'ar_page_id' => $id
2769  ], __METHOD__
2770  );
2771 
2772  // Clone the title and wikiPage, so we have the information we need when
2773  // we log and run the ArticleDeleteComplete hook.
2774  $logTitle = clone $this->mTitle;
2775  $wikiPageBeforeDelete = clone $this;
2776 
2777  // Now that it's safely backed up, delete it
2778  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2779 
2780  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2781  $logtype = $suppress ? 'suppress' : 'delete';
2782 
2783  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2784  $logEntry->setPerformer( $deleter );
2785  $logEntry->setTarget( $logTitle );
2786  $logEntry->setComment( $reason );
2787  $logEntry->setTags( $tags );
2788  $logid = $logEntry->insert();
2789 
2790  $dbw->onTransactionPreCommitOrIdle(
2791  function () use ( $logEntry, $logid ) {
2792  // T58776: avoid deadlocks (especially from FileDeleteForm)
2793  $logEntry->publish( $logid );
2794  },
2795  __METHOD__
2796  );
2797 
2798  $dbw->endAtomic( __METHOD__ );
2799 
2800  $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2801 
2802  Hooks::run( 'ArticleDeleteComplete', [
2803  &$wikiPageBeforeDelete,
2804  &$deleter,
2805  $reason,
2806  $id,
2807  $content,
2808  $logEntry,
2809  $archivedRevisionCount
2810  ] );
2811  $status->value = $logid;
2812 
2813  // Show log excerpt on 404 pages rather than just a link
2814  $dbCache = ObjectCache::getInstance( 'db-replicated' );
2815  $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2816  $dbCache->set( $key, 1, $dbCache::TTL_DAY );
2817  }
2818 
2819  return $status;
2820  }
2821 
2831  protected function archiveRevisions( $dbw, $id, $suppress ) {
2834 
2835  // Given the lock above, we can be confident in the title and page ID values
2836  $namespace = $this->getTitle()->getNamespace();
2837  $dbKey = $this->getTitle()->getDBkey();
2838 
2839  $commentStore = CommentStore::getStore();
2840  $actorMigration = ActorMigration::newMigration();
2841 
2843  $bitfield = false;
2844 
2845  // Bitfields to further suppress the content
2846  if ( $suppress ) {
2847  $bitfield = RevisionRecord::SUPPRESSED_ALL;
2848  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2849  }
2850 
2851  // For now, shunt the revision data into the archive table.
2852  // Text is *not* removed from the text table; bulk storage
2853  // is left intact to avoid breaking block-compression or
2854  // immutable storage schemes.
2855  // In the future, we may keep revisions and mark them with
2856  // the rev_deleted field, which is reserved for this purpose.
2857 
2858  // Lock rows in `revision` and its temp tables, but not any others.
2859  // Note array_intersect() preserves keys from the first arg, and we're
2860  // assuming $revQuery has `revision` primary and isn't using subtables
2861  // for anything we care about.
2862  $dbw->lockForUpdate(
2863  array_intersect(
2864  $revQuery['tables'],
2865  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2866  ),
2867  [ 'rev_page' => $id ],
2868  __METHOD__,
2869  [],
2870  $revQuery['joins']
2871  );
2872 
2873  // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
2874  // so we can copy it to the archive table.
2875  // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
2876  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2877  $revQuery['fields'][] = 'rev_text_id';
2878 
2879  if ( $wgContentHandlerUseDB ) {
2880  $revQuery['fields'][] = 'rev_content_model';
2881  $revQuery['fields'][] = 'rev_content_format';
2882  }
2883  }
2884 
2885  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2886  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2887  $res = $dbw->select(
2888  $revQuery['tables'],
2889  $revQuery['fields'],
2890  [ 'rev_page' => $id ],
2891  __METHOD__,
2892  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2893  $revQuery['joins']
2894  );
2895 
2896  // Build their equivalent archive rows
2897  $rowsInsert = [];
2898  $revids = [];
2899 
2901  $ipRevIds = [];
2902 
2903  $done = true;
2904  foreach ( $res as $row ) {
2905  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2906  $done = false;
2907  break;
2908  }
2909 
2910  $comment = $commentStore->getComment( 'rev_comment', $row );
2911  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2912  $rowInsert = [
2913  'ar_namespace' => $namespace,
2914  'ar_title' => $dbKey,
2915  'ar_timestamp' => $row->rev_timestamp,
2916  'ar_minor_edit' => $row->rev_minor_edit,
2917  'ar_rev_id' => $row->rev_id,
2918  'ar_parent_id' => $row->rev_parent_id,
2927  'ar_len' => $row->rev_len,
2928  'ar_page_id' => $id,
2929  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2930  'ar_sha1' => $row->rev_sha1,
2931  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2932  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2933 
2934  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2935  $rowInsert['ar_text_id'] = $row->rev_text_id;
2936 
2937  if ( $wgContentHandlerUseDB ) {
2938  $rowInsert['ar_content_model'] = $row->rev_content_model;
2939  $rowInsert['ar_content_format'] = $row->rev_content_format;
2940  }
2941  }
2942 
2943  $rowsInsert[] = $rowInsert;
2944  $revids[] = $row->rev_id;
2945 
2946  // Keep track of IP edits, so that the corresponding rows can
2947  // be deleted in the ip_changes table.
2948  if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2949  $ipRevIds[] = $row->rev_id;
2950  }
2951  }
2952 
2953  // This conditional is just a sanity check
2954  if ( count( $revids ) > 0 ) {
2955  // Copy them into the archive table
2956  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2957 
2958  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2959  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2960  if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
2961  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2962  }
2963 
2964  // Also delete records from ip_changes as applicable.
2965  if ( count( $ipRevIds ) > 0 ) {
2966  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2967  }
2968  }
2969 
2970  return $done;
2971  }
2972 
2979  public function lockAndGetLatest() {
2980  return (int)wfGetDB( DB_MASTER )->selectField(
2981  'page',
2982  'page_latest',
2983  [
2984  'page_id' => $this->getId(),
2985  // Typically page_id is enough, but some code might try to do
2986  // updates assuming the title is the same, so verify that
2987  'page_namespace' => $this->getTitle()->getNamespace(),
2988  'page_title' => $this->getTitle()->getDBkey()
2989  ],
2990  __METHOD__,
2991  [ 'FOR UPDATE' ]
2992  );
2993  }
2994 
3007  public function doDeleteUpdates(
3008  $id, Content $content = null, Revision $revision = null, User $user = null
3009  ) {
3010  if ( $id !== $this->getId() ) {
3011  throw new InvalidArgumentException( 'Mismatching page ID' );
3012  }
3013 
3014  try {
3015  $countable = $this->isCountable();
3016  } catch ( Exception $ex ) {
3017  // fallback for deleting broken pages for which we cannot load the content for
3018  // some reason. Note that doDeleteArticleReal() already logged this problem.
3019  $countable = false;
3020  }
3021 
3022  // Update site status
3024  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3025  ) );
3026 
3027  // Delete pagelinks, update secondary indexes, etc
3028  $updates = $this->getDeletionUpdates(
3029  $revision ? $revision->getRevisionRecord() : $content
3030  );
3031  foreach ( $updates as $update ) {
3032  DeferredUpdates::addUpdate( $update );
3033  }
3034 
3035  $causeAgent = $user ? $user->getName() : 'unknown';
3036  // Reparse any pages transcluding this page
3038  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3039  // Reparse any pages including this image
3040  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3042  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3043  }
3044 
3045  // Clear caches
3046  self::onArticleDelete( $this->mTitle );
3048  $this->mTitle,
3049  $revision,
3050  null,
3052  );
3053 
3054  // Reset this object and the Title object
3055  $this->loadFromRow( false, self::READ_LATEST );
3056 
3057  // Search engine
3058  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3059  }
3060 
3090  public function doRollback(
3091  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3092  ) {
3093  $resultDetails = null;
3094 
3095  // Check permissions
3096  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3097  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3098  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3099 
3100  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3101  $errors[] = [ 'sessionfailure' ];
3102  }
3103 
3104  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3105  $errors[] = [ 'actionthrottledtext' ];
3106  }
3107 
3108  // If there were errors, bail out now
3109  if ( !empty( $errors ) ) {
3110  return $errors;
3111  }
3112 
3113  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3114  }
3115 
3136  public function commitRollback( $fromP, $summary, $bot,
3137  &$resultDetails, User $guser, $tags = null
3138  ) {
3139  global $wgUseRCPatrol;
3140 
3141  $dbw = wfGetDB( DB_MASTER );
3142 
3143  if ( wfReadOnly() ) {
3144  return [ [ 'readonlytext' ] ];
3145  }
3146 
3147  // Begin revision creation cycle by creating a PageUpdater.
3148  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3149  $updater = $this->newPageUpdater( $guser );
3150  $current = $updater->grabParentRevision();
3151 
3152  if ( is_null( $current ) ) {
3153  // Something wrong... no page?
3154  return [ [ 'notanarticle' ] ];
3155  }
3156 
3157  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3158  $legacyCurrent = new Revision( $current );
3159  $from = str_replace( '_', ' ', $fromP );
3160 
3161  // User name given should match up with the top revision.
3162  // If the revision's user is not visible, then $from should be empty.
3163  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3164  $resultDetails = [ 'current' => $legacyCurrent ];
3165  return [ [ 'alreadyrolled',
3166  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3167  htmlspecialchars( $fromP ),
3168  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3169  ] ];
3170  }
3171 
3172  // Get the last edit not by this person...
3173  // Note: these may not be public values
3174  $actorWhere = ActorMigration::newMigration()->getWhere(
3175  $dbw,
3176  'rev_user',
3177  $current->getUser( RevisionRecord::RAW )
3178  );
3179 
3180  $s = $dbw->selectRow(
3181  [ 'revision' ] + $actorWhere['tables'],
3182  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3183  [
3184  'rev_page' => $current->getPageId(),
3185  'NOT(' . $actorWhere['conds'] . ')',
3186  ],
3187  __METHOD__,
3188  [
3189  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3190  'ORDER BY' => 'rev_timestamp DESC'
3191  ],
3192  $actorWhere['joins']
3193  );
3194  if ( $s === false ) {
3195  // No one else ever edited this page
3196  return [ [ 'cantrollback' ] ];
3197  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3198  || $s->rev_deleted & RevisionRecord::DELETED_USER
3199  ) {
3200  // Only admins can see this text
3201  return [ [ 'notvisiblerev' ] ];
3202  }
3203 
3204  // Generate the edit summary if necessary
3205  $target = $this->getRevisionStore()->getRevisionById(
3206  $s->rev_id,
3207  RevisionStore::READ_LATEST
3208  );
3209  if ( empty( $summary ) ) {
3210  if ( !$currentEditorForPublic ) { // no public user name
3211  $summary = wfMessage( 'revertpage-nouser' );
3212  } else {
3213  $summary = wfMessage( 'revertpage' );
3214  }
3215  }
3216  $legacyTarget = new Revision( $target );
3217  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3218 
3219  // Allow the custom summary to use the same args as the default message
3220  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3221  $args = [
3222  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3223  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3224  $s->rev_id,
3225  $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3226  $current->getId(),
3227  $contLang->timeanddate( $current->getTimestamp() )
3228  ];
3229  if ( $summary instanceof Message ) {
3230  $summary = $summary->params( $args )->inContentLanguage()->text();
3231  } else {
3232  $summary = wfMsgReplaceArgs( $summary, $args );
3233  }
3234 
3235  // Trim spaces on user supplied text
3236  $summary = trim( $summary );
3237 
3238  // Save
3239  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3240 
3241  if ( $guser->isAllowed( 'minoredit' ) ) {
3242  $flags |= EDIT_MINOR;
3243  }
3244 
3245  if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3246  $flags |= EDIT_FORCE_BOT;
3247  }
3248 
3249  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3250  $currentContent = $current->getContent( SlotRecord::MAIN );
3251  $targetContent = $target->getContent( SlotRecord::MAIN );
3252  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3253 
3254  if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3255  $tags[] = 'mw-rollback';
3256  }
3257 
3258  // Build rollback revision:
3259  // Restore old content
3260  // TODO: MCR: test this once we can store multiple slots
3261  foreach ( $target->getSlots()->getSlots() as $slot ) {
3262  $updater->inheritSlot( $slot );
3263  }
3264 
3265  // Remove extra slots
3266  // TODO: MCR: test this once we can store multiple slots
3267  foreach ( $current->getSlotRoles() as $role ) {
3268  if ( !$target->hasSlot( $role ) ) {
3269  $updater->removeSlot( $role );
3270  }
3271  }
3272 
3273  $updater->setOriginalRevisionId( $target->getId() );
3274  // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3275  $updater->addTags( $tags );
3276 
3277  // TODO: this logic should not be in the storage layer, it's here for compatibility
3278  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3279  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3280  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
3281 
3282  if ( $wgUseRCPatrol && $permissionManager->userCan(
3283  'autopatrol', $guser, $this->getTitle()
3284  ) ) {
3285  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3286  }
3287 
3288  // Actually store the rollback
3289  $rev = $updater->saveRevision(
3291  $flags
3292  );
3293 
3294  // Set patrolling and bot flag on the edits, which gets rollbacked.
3295  // This is done even on edit failure to have patrolling in that case (T64157).
3296  $set = [];
3297  if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3298  // Mark all reverted edits as bot
3299  $set['rc_bot'] = 1;
3300  }
3301 
3302  if ( $wgUseRCPatrol ) {
3303  // Mark all reverted edits as patrolled
3304  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3305  }
3306 
3307  if ( count( $set ) ) {
3308  $actorWhere = ActorMigration::newMigration()->getWhere(
3309  $dbw,
3310  'rc_user',
3311  $current->getUser( RevisionRecord::RAW ),
3312  false
3313  );
3314  $dbw->update( 'recentchanges', $set,
3315  [ /* WHERE */
3316  'rc_cur_id' => $current->getPageId(),
3317  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3318  $actorWhere['conds'], // No tables/joins are needed for rc_user
3319  ],
3320  __METHOD__
3321  );
3322  }
3323 
3324  if ( !$updater->wasSuccessful() ) {
3325  return $updater->getStatus()->getErrorsArray();
3326  }
3327 
3328  // Report if the edit was not created because it did not change the content.
3329  if ( $updater->isUnchanged() ) {
3330  $resultDetails = [ 'current' => $legacyCurrent ];
3331  return [ [ 'alreadyrolled',
3332  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3333  htmlspecialchars( $fromP ),
3334  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3335  ] ];
3336  }
3337 
3338  if ( $changingContentModel ) {
3339  // If the content model changed during the rollback,
3340  // make sure it gets logged to Special:Log/contentmodel
3341  $log = new ManualLogEntry( 'contentmodel', 'change' );
3342  $log->setPerformer( $guser );
3343  $log->setTarget( $this->mTitle );
3344  $log->setComment( $summary );
3345  $log->setParameters( [
3346  '4::oldmodel' => $currentContent->getModel(),
3347  '5::newmodel' => $targetContent->getModel(),
3348  ] );
3349 
3350  $logId = $log->insert( $dbw );
3351  $log->publish( $logId );
3352  }
3353 
3354  $revId = $rev->getId();
3355 
3356  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3357 
3358  $resultDetails = [
3359  'summary' => $summary,
3360  'current' => $legacyCurrent,
3361  'target' => $legacyTarget,
3362  'newid' => $revId,
3363  'tags' => $tags
3364  ];
3365 
3366  // TODO: make this return a Status object and wrap $resultDetails in that.
3367  return [];
3368  }
3369 
3381  public static function onArticleCreate( Title $title ) {
3382  // TODO: move this into a PageEventEmitter service
3383 
3384  // Update existence markers on article/talk tabs...
3385  $other = $title->getOtherPage();
3386 
3387  $other->purgeSquid();
3388 
3389  $title->touchLinks();
3390  $title->purgeSquid();
3391  $title->deleteTitleProtection();
3392 
3393  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3394 
3395  // Invalidate caches of articles which include this page
3397  new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
3398  );
3399 
3400  if ( $title->getNamespace() == NS_CATEGORY ) {
3401  // Load the Category object, which will schedule a job to create
3402  // the category table row if necessary. Checking a replica DB is ok
3403  // here, in the worst case it'll run an unnecessary recount job on
3404  // a category that probably doesn't have many members.
3405  Category::newFromTitle( $title )->getID();
3406  }
3407  }
3408 
3414  public static function onArticleDelete( Title $title ) {
3415  // TODO: move this into a PageEventEmitter service
3416 
3417  // Update existence markers on article/talk tabs...
3418  // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3419  BacklinkCache::get( $title )->clear();
3420  $other = $title->getOtherPage();
3421 
3422  $other->purgeSquid();
3423 
3424  $title->touchLinks();
3425  $title->purgeSquid();
3426 
3427  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3428 
3429  // File cache
3431  InfoAction::invalidateCache( $title );
3432 
3433  // Messages
3434  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3435  MessageCache::singleton()->updateMessageOverride( $title, null );
3436  }
3437 
3438  // Images
3439  if ( $title->getNamespace() == NS_FILE ) {
3441  new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
3442  );
3443  }
3444 
3445  // User talk pages
3446  if ( $title->getNamespace() == NS_USER_TALK ) {
3447  $user = User::newFromName( $title->getText(), false );
3448  if ( $user ) {
3449  $user->setNewtalk( false );
3450  }
3451  }
3452 
3453  // Image redirects
3454  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3455 
3456  // Purge cross-wiki cache entities referencing this page
3457  self::purgeInterwikiCheckKey( $title );
3458  }
3459 
3468  public static function onArticleEdit(
3469  Title $title,
3470  Revision $revision = null,
3471  $slotsChanged = null
3472  ) {
3473  // TODO: move this into a PageEventEmitter service
3474 
3475  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3476  // Invalidate caches of articles which include this page.
3477  // Only for the main slot, because only the main slot is transcluded.
3478  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3480  new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
3481  );
3482  }
3483 
3484  // Invalidate the caches of all pages which redirect here
3486  new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
3487  );
3488 
3489  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3490 
3491  // Purge CDN for this page only
3492  $title->purgeSquid();
3493  // Clear file cache for this page only
3495 
3496  // Purge ?action=info cache
3497  $revid = $revision ? $revision->getId() : null;
3498  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3499  InfoAction::invalidateCache( $title, $revid );
3500  } );
3501 
3502  // Purge cross-wiki cache entities referencing this page
3503  self::purgeInterwikiCheckKey( $title );
3504  }
3505 
3513  private static function purgeInterwikiCheckKey( Title $title ) {
3515 
3516  if ( !$wgEnableScaryTranscluding ) {
3517  return; // @todo: perhaps this wiki is only used as a *source* for content?
3518  }
3519 
3520  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3521  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3522  $cache->resetCheckKey(
3523  // Do not include the namespace since there can be multiple aliases to it
3524  // due to different namespace text definitions on different wikis. This only
3525  // means that some cache invalidations happen that are not strictly needed.
3526  $cache->makeGlobalKey(
3527  'interwiki-page',
3529  $title->getDBkey()
3530  )
3531  );
3532  } );
3533  }
3534 
3541  public function getCategories() {
3542  $id = $this->getId();
3543  if ( $id == 0 ) {
3544  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3545  }
3546 
3547  $dbr = wfGetDB( DB_REPLICA );
3548  $res = $dbr->select( 'categorylinks',
3549  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3550  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3551  // as not being aliases, and NS_CATEGORY is numeric
3552  [ 'cl_from' => $id ],
3553  __METHOD__ );
3554 
3555  return TitleArray::newFromResult( $res );
3556  }
3557 
3564  public function getHiddenCategories() {
3565  $result = [];
3566  $id = $this->getId();
3567 
3568  if ( $id == 0 ) {
3569  return [];
3570  }
3571 
3572  $dbr = wfGetDB( DB_REPLICA );
3573  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3574  [ 'cl_to' ],
3575  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3576  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3577  __METHOD__ );
3578 
3579  if ( $res !== false ) {
3580  foreach ( $res as $row ) {
3581  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3582  }
3583  }
3584 
3585  return $result;
3586  }
3587 
3595  public function getAutoDeleteReason( &$hasHistory ) {
3596  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3597  }
3598 
3609  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3610  $id = $id ?: $this->getId();
3611  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3612  getCategoryLinkType( $this->getTitle()->getNamespace() );
3613 
3614  $addFields = [ 'cat_pages = cat_pages + 1' ];
3615  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3616  if ( $type !== 'page' ) {
3617  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3618  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3619  }
3620 
3621  $dbw = wfGetDB( DB_MASTER );
3622 
3623  if ( count( $added ) ) {
3624  $existingAdded = $dbw->selectFieldValues(
3625  'category',
3626  'cat_title',
3627  [ 'cat_title' => $added ],
3628  __METHOD__
3629  );
3630 
3631  // For category rows that already exist, do a plain
3632  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3633  // to avoid creating gaps in the cat_id sequence.
3634  if ( count( $existingAdded ) ) {
3635  $dbw->update(
3636  'category',
3637  $addFields,
3638  [ 'cat_title' => $existingAdded ],
3639  __METHOD__
3640  );
3641  }
3642 
3643  $missingAdded = array_diff( $added, $existingAdded );
3644  if ( count( $missingAdded ) ) {
3645  $insertRows = [];
3646  foreach ( $missingAdded as $cat ) {
3647  $insertRows[] = [
3648  'cat_title' => $cat,
3649  'cat_pages' => 1,
3650  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3651  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3652  ];
3653  }
3654  $dbw->upsert(
3655  'category',
3656  $insertRows,
3657  [ 'cat_title' ],
3658  $addFields,
3659  __METHOD__
3660  );
3661  }
3662  }
3663 
3664  if ( count( $deleted ) ) {
3665  $dbw->update(
3666  'category',
3667  $removeFields,
3668  [ 'cat_title' => $deleted ],
3669  __METHOD__
3670  );
3671  }
3672 
3673  foreach ( $added as $catName ) {
3674  $cat = Category::newFromName( $catName );
3675  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3676  }
3677 
3678  foreach ( $deleted as $catName ) {
3679  $cat = Category::newFromName( $catName );
3680  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3681  // Refresh counts on categories that should be empty now (after commit, T166757)
3682  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3683  $cat->refreshCountsIfEmpty();
3684  } );
3685  }
3686  }
3687 
3694  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3695  if ( wfReadOnly() ) {
3696  return;
3697  }
3698 
3699  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3700  [ $this, $this->mTitle, $parserOutput ]
3701  ) ) {
3702  return;
3703  }
3704 
3705  $config = RequestContext::getMain()->getConfig();
3706 
3707  $params = [
3708  'isOpportunistic' => true,
3709  'rootJobTimestamp' => $parserOutput->getCacheTime()
3710  ];
3711 
3712  if ( $this->mTitle->areRestrictionsCascading() ) {
3713  // If the page is cascade protecting, the links should really be up-to-date
3714  JobQueueGroup::singleton()->lazyPush(
3715  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3716  );
3717  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3718  // Assume the output contains "dynamic" time/random based magic words.
3719  // Only update pages that expired due to dynamic content and NOT due to edits
3720  // to referenced templates/files. When the cache expires due to dynamic content,
3721  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3722  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3723  // template/file edit already triggered recursive RefreshLinksJob jobs.
3724  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3725  // If a page is uncacheable, do not keep spamming a job for it.
3726  // Although it would be de-duplicated, it would still waste I/O.
3728  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3729  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3730  if ( $cache->add( $key, time(), $ttl ) ) {
3731  JobQueueGroup::singleton()->lazyPush(
3732  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3733  );
3734  }
3735  }
3736  }
3737  }
3738 
3748  public function getDeletionUpdates( $rev = null ) {
3749  if ( !$rev ) {
3750  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3751 
3752  try {
3753  $rev = $this->getRevisionRecord();
3754  } catch ( Exception $ex ) {
3755  // If we can't load the content, something is wrong. Perhaps that's why
3756  // the user is trying to delete the page, so let's not fail in that case.
3757  // Note that doDeleteArticleReal() will already have logged an issue with
3758  // loading the content.
3759  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3760  }
3761  }
3762 
3763  if ( !$rev ) {
3764  $slotContent = [];
3765  } elseif ( $rev instanceof Content ) {
3766  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3767 
3768  $slotContent = [ SlotRecord::MAIN => $rev ];
3769  } else {
3770  $slotContent = array_map( function ( SlotRecord $slot ) {
3771  return $slot->getContent( RevisionRecord::RAW );
3772  }, $rev->getSlots()->getSlots() );
3773  }
3774 
3775  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3776 
3777  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3778  // model here, not the content object!
3779  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3781  foreach ( $slotContent as $role => $content ) {
3782  $handler = $content->getContentHandler();
3783 
3784  $updates = $handler->getDeletionUpdates(
3785  $this->getTitle(),
3786  $role
3787  );
3788  $allUpdates = array_merge( $allUpdates, $updates );
3789 
3790  // TODO: remove B/C hack in 1.32!
3791  $legacyUpdates = $content->getDeletionUpdates( $this );
3792 
3793  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3794  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3795  return !( $update instanceof LinksDeletionUpdate );
3796  } );
3797 
3798  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3799  }
3800 
3801  Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3802 
3803  // TODO: hard deprecate old hook in 1.33
3804  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3805  return $allUpdates;
3806  }
3807 
3815  public function isLocal() {
3816  return true;
3817  }
3818 
3828  public function getWikiDisplayName() {
3829  global $wgSitename;
3830  return $wgSitename;
3831  }
3832 
3841  public function getSourceURL() {
3842  return $this->getTitle()->getCanonicalURL();
3843  }
3844 
3851  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3852 
3853  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3854  }
3855 
3856 }
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:699
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:264
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:138
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:2873
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2077
setLastEdit(Revision $revision)
Set the latest revision.
Definition: WikiPage.php:774
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:3381
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4246
$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:1612
string $mLinksUpdated
Definition: WikiPage.php:107
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:710
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3828
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:485
getParserCache()
Definition: WikiPage.php:256
string $mTouched
Definition: WikiPage.php:102
$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:629
static invalidateModuleCache(Title $title, Revision $old=null, Revision $new=null, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:1002
clearNotification(&$title, $oldid=0)
Clear the user&#39;s notification timestamp for the given title.
Definition: User.php:3750
int $mId
Definition: WikiPage.php:77
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:795
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:263
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:1972
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:82
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:3609
getTimestamp()
Definition: Revision.php:994
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3607
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2472
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1042
Title $mTitle
Definition: WikiPage.php:51
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage.
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3513
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:1113
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
const EDIT_INTERNAL
Definition: Defines.php:139
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:295
clear()
Clear the object.
Definition: WikiPage.php:302
Handles purging appropriate CDN URLs given a title (or titles)
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:627
$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:3694
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3841
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1329
$source
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:925
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:156
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:1591
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:92
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2664
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:1151
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
newDerivedDataUpdater()
Definition: WikiPage.php:1685
const EDIT_MINOR
Definition: Defines.php:134
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:133
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:521
getTouched()
Get the page_touched field.
Definition: WikiPage.php:688
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:841
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:97
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1624
$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:313
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:734
DerivedPageDataUpdater null $derivedDataUpdater
Definition: WikiPage.php:112
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3850
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:960
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2251
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:455
getActionOverrides()
Definition: WikiPage.php:273
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:630
Class DeletePageJob.
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
getRevisionStore()
Definition: WikiPage.php:235
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. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\*-\*)") will be honored when streaming the file. '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:1970
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:648
static onArticleEdit(Title $title, Revision $revision=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3468
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:1453
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:677
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility...
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password 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:767
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:3136
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:2626
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
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 '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:1244
getRevision()
Get the latest revision.
Definition: WikiPage.php:783
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
getRevisionRenderer()
Definition: WikiPage.php:242
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:1790
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:24
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:71
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:2426
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:2564
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
const EDIT_FORCE_BOT
Definition: Defines.php:136
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:2593
doDeleteUpdates( $id, Content $content=null, Revision $revision=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:3007
Revision $mLastRevision
Definition: WikiPage.php:92
getUserText( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:892
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:1020
updateIfNewerOn( $dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record...
Definition: WikiPage.php:1488
$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:1325
getSlotRoleRegistry()
Definition: WikiPage.php:249
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:266
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:1102
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1585
const NS_MEDIA
Definition: Defines.php:48
$res
Definition: database.txt:21
static singleton()
Definition: RepoGroup.php:60
getContentHandler()
Returns the content handler appropriate for this revision&#39;s content model.
Definition: Revision.php:987
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:57
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:126
$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:72
getCacheTime()
Definition: CacheTime.php:60
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:721
$cache
Definition: mcc.php:33
$params
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3564
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1264
getTitle()
Get the title object of the article.
Definition: WikiPage.php:294
const NS_CATEGORY
Definition: Defines.php:74
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3642
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:1972
getComment( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:911
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:57
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:767
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:912
hasViewableContent()
Check if this page is something we&#39;re going to be showing some sort of sensible content for...
Definition: WikiPage.php:621
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object...
Definition: Revision.php:511
$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:1044
const NS_FILE
Definition: Defines.php:66
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:1748
int false $mLatest
False means "not loaded".
Definition: WikiPage.php:69
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:930
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:556
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:602
Special handling for file pages.
const NS_MEDIAWIKI
Definition: Defines.php:68
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:286
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1287
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2121
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3595
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2407
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:1545
bool $mDataLoaded
Definition: WikiPage.php:57
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:1950
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:942
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:591
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
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:176
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
static newDynamic(Title $title, array $params)
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:559
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
wfRandom()
Get a random decimal value in the domain of [0, 1), in a way not likely to give duplicate values for ...
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:3541
static convertSelectType( $type)
Convert &#39;fromdb&#39;, &#39;fromdbmaster&#39; and &#39;forupdate&#39; to READ_* constants.
Definition: WikiPage.php:218
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:2049
getId()
Get the user&#39;s ID.
Definition: User.php:2224
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4452
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1928
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3815
const EDIT_NEW
Definition: Defines.php:132
getTimestamp()
Definition: WikiPage.php:827
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:469
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:1734
static hasDifferencesOutsideMainSlot(Revision $a, Revision $b)
Helper method for checking whether two revisions have differences that go beyond the main slot...
Definition: WikiPage.php:1526
getUser( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:854
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:3748
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3414
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:1567
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:3090
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:1670
$revQuery
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:422
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:816
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1201
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition: hooks.txt:2621
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:995
formatExpiry( $expiry)
Definition: WikiPage.php:2450
bool $mIsRedirect
Definition: WikiPage.php:63
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:1067
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1223
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1374
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:2537
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:2979
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:1871
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:535
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:118
static selectFields()
Return the list of revision fields that should be selected to create a new page.
Definition: WikiPage.php:344
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:333
const PRC_AUTOPATROLLED
$content
Definition: pageupdater.txt:72
const NS_USER_TALK
Definition: Defines.php:63
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:1980
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Title $mRedirectTarget
Definition: WikiPage.php:87
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:118
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page&#39;s history.
Definition: Revision.php:1196
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object...
Definition: WikiPage.php:383
getCreator( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:873
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:2831
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:206
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition: WikiPage.php:530
Special handling for category pages.
static singleton()
Get the signleton instance of this class.
purgeSquid()
Purge all applicable CDN URLs.
Definition: Title.php:3435
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1454
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:2151
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2514