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 = Revision::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 = Revision::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 = Revision::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 = Revision::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 = Revision::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', Revision::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->getConnection( 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->getConnection( 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->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership );
1701  $derivedDataUpdater->setArticleCountMethod( $wgArticleCountMethod );
1702 
1703  return $derivedDataUpdater;
1704  }
1705 
1733  private function getDerivedDataUpdater(
1734  User $forUser = null,
1735  RevisionRecord $forRevision = null,
1736  RevisionSlotsUpdate $forUpdate = null,
1737  $forEdit = false
1738  ) {
1739  if ( !$forRevision && !$forUpdate ) {
1740  // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1741  // going to use it with.
1742  $this->derivedDataUpdater = null;
1743  }
1744 
1745  if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1746  // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1747  // to it did not yet initialize it, because we don't know what data it will be
1748  // initialized with.
1749  $this->derivedDataUpdater = null;
1750  }
1751 
1752  // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1753  // However, there is no good way to construct a cache key. We'd need to check against all
1754  // cached instances.
1755 
1756  if ( $this->derivedDataUpdater
1757  && !$this->derivedDataUpdater->isReusableFor(
1758  $forUser,
1759  $forRevision,
1760  $forUpdate,
1761  $forEdit ? $this->getLatest() : null
1762  )
1763  ) {
1764  $this->derivedDataUpdater = null;
1765  }
1766 
1767  if ( !$this->derivedDataUpdater ) {
1768  $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1769  }
1770 
1772  }
1773 
1789  public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1791 
1792  $pageUpdater = new PageUpdater(
1793  $user,
1794  $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1795  $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1796  $this->getDBLoadBalancer(),
1797  $this->getRevisionStore(),
1798  $this->getSlotRoleRegistry()
1799  );
1800 
1801  $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
1802  $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
1803  $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
1804 
1805  return $pageUpdater;
1806  }
1807 
1870  public function doEditContent(
1871  Content $content, $summary, $flags = 0, $originalRevId = false,
1872  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1873  ) {
1874  global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1875 
1876  if ( !( $summary instanceof CommentStoreComment ) ) {
1877  $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1878  }
1879 
1880  if ( !$user ) {
1881  $user = $wgUser;
1882  }
1883 
1884  // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1885  // Checking the minoredit right should be done in the same place the 'bot' right is
1886  // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1887  if ( ( $flags & EDIT_MINOR ) && !$user->isAllowed( 'minoredit' ) ) {
1888  $flags = ( $flags & ~EDIT_MINOR );
1889  }
1890 
1891  $slotsUpdate = new RevisionSlotsUpdate();
1892  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1893 
1894  // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1895  // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1896  // used by this PageUpdater. However, there is no guarantee for this.
1897  $updater = $this->newPageUpdater( $user, $slotsUpdate );
1898  $updater->setContent( SlotRecord::MAIN, $content );
1899  $updater->setOriginalRevisionId( $originalRevId );
1900  $updater->setUndidRevisionId( $undidRevId );
1901 
1902  $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1903 
1904  // TODO: this logic should not be in the storage layer, it's here for compatibility
1905  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1906  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1907  if ( $needsPatrol && $this->getTitle()->userCan( 'autopatrol', $user ) ) {
1908  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1909  }
1910 
1911  $updater->addTags( $tags );
1912 
1913  $revRec = $updater->saveRevision(
1914  $summary,
1915  $flags
1916  );
1917 
1918  // $revRec will be null if the edit failed, or if no new revision was created because
1919  // the content did not change.
1920  if ( $revRec ) {
1921  // update cached fields
1922  // TODO: this is currently redundant to what is done in updateRevisionOn.
1923  // But updateRevisionOn() should move into PageStore, and then this will be needed.
1924  $this->setLastEdit( new Revision( $revRec ) ); // TODO: use RevisionRecord
1925  $this->mLatest = $revRec->getId();
1926  }
1927 
1928  return $updater->getStatus();
1929  }
1930 
1945  public function makeParserOptions( $context ) {
1947 
1948  if ( $this->getTitle()->isConversionTable() ) {
1949  // @todo ConversionTable should become a separate content model, so
1950  // we don't need special cases like this one.
1951  $options->disableContentConversion();
1952  }
1953 
1954  return $options;
1955  }
1956 
1975  public function prepareContentForEdit(
1976  Content $content,
1977  $revision = null,
1978  User $user = null,
1979  $serialFormat = null,
1980  $useCache = true
1981  ) {
1982  global $wgUser;
1983 
1984  if ( !$user ) {
1985  $user = $wgUser;
1986  }
1987 
1988  if ( $revision !== null ) {
1989  if ( $revision instanceof Revision ) {
1990  $revision = $revision->getRevisionRecord();
1991  } elseif ( !( $revision instanceof RevisionRecord ) ) {
1992  throw new InvalidArgumentException(
1993  __METHOD__ . ': invalid $revision argument type ' . gettype( $revision ) );
1994  }
1995  }
1996 
1997  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
1998  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
1999 
2000  if ( !$updater->isUpdatePrepared() ) {
2001  $updater->prepareContent( $user, $slots, $useCache );
2002 
2003  if ( $revision ) {
2004  $updater->prepareUpdate(
2005  $revision,
2006  [
2007  'causeAction' => 'prepare-edit',
2008  'causeAgent' => $user->getName(),
2009  ]
2010  );
2011  }
2012  }
2013 
2014  return $updater->getPreparedEdit();
2015  }
2016 
2044  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2045  $options += [
2046  'causeAction' => 'edit-page',
2047  'causeAgent' => $user->getName(),
2048  ];
2049 
2050  $revision = $revision->getRevisionRecord();
2051 
2052  $updater = $this->getDerivedDataUpdater( $user, $revision );
2053 
2054  $updater->prepareUpdate( $revision, $options );
2055 
2056  $updater->doUpdates();
2057  }
2058 
2072  public function updateParserCache( array $options = [] ) {
2073  $revision = $this->getRevisionRecord();
2074  if ( !$revision || !$revision->getId() ) {
2075  LoggerFactory::getInstance( 'wikipage' )->info(
2076  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2077  );
2078  return;
2079  }
2080  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2081 
2082  $updater = $this->getDerivedDataUpdater( $user, $revision );
2083  $updater->prepareUpdate( $revision, $options );
2084  $updater->doParserCacheUpdate();
2085  }
2086 
2113  public function doSecondaryDataUpdates( array $options = [] ) {
2114  $options['recursive'] = $options['recursive'] ?? true;
2115  $revision = $this->getRevisionRecord();
2116  if ( !$revision || !$revision->getId() ) {
2117  LoggerFactory::getInstance( 'wikipage' )->info(
2118  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2119  );
2120  return;
2121  }
2122  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2123 
2124  $updater = $this->getDerivedDataUpdater( $user, $revision );
2125  $updater->prepareUpdate( $revision, $options );
2126  $updater->doSecondaryDataUpdates( $options );
2127  }
2128 
2143  public function doUpdateRestrictions( array $limit, array $expiry,
2144  &$cascade, $reason, User $user, $tags = null
2145  ) {
2147 
2148  if ( wfReadOnly() ) {
2149  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2150  }
2151 
2152  $this->loadPageData( 'fromdbmaster' );
2153  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2154  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2155  $id = $this->getId();
2156 
2157  if ( !$cascade ) {
2158  $cascade = false;
2159  }
2160 
2161  // Take this opportunity to purge out expired restrictions
2163 
2164  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2165  // we expect a single selection, but the schema allows otherwise.
2166  $isProtected = false;
2167  $protect = false;
2168  $changed = false;
2169 
2170  $dbw = wfGetDB( DB_MASTER );
2171 
2172  foreach ( $restrictionTypes as $action ) {
2173  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2174  $expiry[$action] = 'infinity';
2175  }
2176  if ( !isset( $limit[$action] ) ) {
2177  $limit[$action] = '';
2178  } elseif ( $limit[$action] != '' ) {
2179  $protect = true;
2180  }
2181 
2182  // Get current restrictions on $action
2183  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2184  if ( $current != '' ) {
2185  $isProtected = true;
2186  }
2187 
2188  if ( $limit[$action] != $current ) {
2189  $changed = true;
2190  } elseif ( $limit[$action] != '' ) {
2191  // Only check expiry change if the action is actually being
2192  // protected, since expiry does nothing on an not-protected
2193  // action.
2194  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2195  $changed = true;
2196  }
2197  }
2198  }
2199 
2200  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2201  $changed = true;
2202  }
2203 
2204  // If nothing has changed, do nothing
2205  if ( !$changed ) {
2206  return Status::newGood();
2207  }
2208 
2209  if ( !$protect ) { // No protection at all means unprotection
2210  $revCommentMsg = 'unprotectedarticle-comment';
2211  $logAction = 'unprotect';
2212  } elseif ( $isProtected ) {
2213  $revCommentMsg = 'modifiedarticleprotection-comment';
2214  $logAction = 'modify';
2215  } else {
2216  $revCommentMsg = 'protectedarticle-comment';
2217  $logAction = 'protect';
2218  }
2219 
2220  $logRelationsValues = [];
2221  $logRelationsField = null;
2222  $logParamsDetails = [];
2223 
2224  // Null revision (used for change tag insertion)
2225  $nullRevision = null;
2226 
2227  if ( $id ) { // Protection of existing page
2228  // Avoid PHP 7.1 warning of passing $this by reference
2229  $wikiPage = $this;
2230 
2231  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2232  return Status::newGood();
2233  }
2234 
2235  // Only certain restrictions can cascade...
2236  $editrestriction = isset( $limit['edit'] )
2237  ? [ $limit['edit'] ]
2238  : $this->mTitle->getRestrictions( 'edit' );
2239  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2240  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2241  }
2242  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2243  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2244  }
2245 
2246  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2247  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2248  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2249  }
2250  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2251  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2252  }
2253 
2254  // The schema allows multiple restrictions
2255  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2256  $cascade = false;
2257  }
2258 
2259  // insert null revision to identify the page protection change as edit summary
2260  $latest = $this->getLatest();
2261  $nullRevision = $this->insertProtectNullRevision(
2262  $revCommentMsg,
2263  $limit,
2264  $expiry,
2265  $cascade,
2266  $reason,
2267  $user
2268  );
2269 
2270  if ( $nullRevision === null ) {
2271  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2272  }
2273 
2274  $logRelationsField = 'pr_id';
2275 
2276  // Update restrictions table
2277  foreach ( $limit as $action => $restrictions ) {
2278  $dbw->delete(
2279  'page_restrictions',
2280  [
2281  'pr_page' => $id,
2282  'pr_type' => $action
2283  ],
2284  __METHOD__
2285  );
2286  if ( $restrictions != '' ) {
2287  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2288  $dbw->insert(
2289  'page_restrictions',
2290  [
2291  'pr_page' => $id,
2292  'pr_type' => $action,
2293  'pr_level' => $restrictions,
2294  'pr_cascade' => $cascadeValue,
2295  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2296  ],
2297  __METHOD__
2298  );
2299  $logRelationsValues[] = $dbw->insertId();
2300  $logParamsDetails[] = [
2301  'type' => $action,
2302  'level' => $restrictions,
2303  'expiry' => $expiry[$action],
2304  'cascade' => (bool)$cascadeValue,
2305  ];
2306  }
2307  }
2308 
2309  // Clear out legacy restriction fields
2310  $dbw->update(
2311  'page',
2312  [ 'page_restrictions' => '' ],
2313  [ 'page_id' => $id ],
2314  __METHOD__
2315  );
2316 
2317  // Avoid PHP 7.1 warning of passing $this by reference
2318  $wikiPage = $this;
2319 
2320  Hooks::run( 'NewRevisionFromEditComplete',
2321  [ $this, $nullRevision, $latest, $user ] );
2322  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2323  } else { // Protection of non-existing page (also known as "title protection")
2324  // Cascade protection is meaningless in this case
2325  $cascade = false;
2326 
2327  if ( $limit['create'] != '' ) {
2328  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2329  $dbw->replace( 'protected_titles',
2330  [ [ 'pt_namespace', 'pt_title' ] ],
2331  [
2332  'pt_namespace' => $this->mTitle->getNamespace(),
2333  'pt_title' => $this->mTitle->getDBkey(),
2334  'pt_create_perm' => $limit['create'],
2335  'pt_timestamp' => $dbw->timestamp(),
2336  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2337  'pt_user' => $user->getId(),
2338  ] + $commentFields, __METHOD__
2339  );
2340  $logParamsDetails[] = [
2341  'type' => 'create',
2342  'level' => $limit['create'],
2343  'expiry' => $expiry['create'],
2344  ];
2345  } else {
2346  $dbw->delete( 'protected_titles',
2347  [
2348  'pt_namespace' => $this->mTitle->getNamespace(),
2349  'pt_title' => $this->mTitle->getDBkey()
2350  ], __METHOD__
2351  );
2352  }
2353  }
2354 
2355  $this->mTitle->flushRestrictions();
2356  InfoAction::invalidateCache( $this->mTitle );
2357 
2358  if ( $logAction == 'unprotect' ) {
2359  $params = [];
2360  } else {
2361  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2362  $params = [
2363  '4::description' => $protectDescriptionLog, // parameter for IRC
2364  '5:bool:cascade' => $cascade,
2365  'details' => $logParamsDetails, // parameter for localize and api
2366  ];
2367  }
2368 
2369  // Update the protection log
2370  $logEntry = new ManualLogEntry( 'protect', $logAction );
2371  $logEntry->setTarget( $this->mTitle );
2372  $logEntry->setComment( $reason );
2373  $logEntry->setPerformer( $user );
2374  $logEntry->setParameters( $params );
2375  if ( !is_null( $nullRevision ) ) {
2376  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2377  }
2378  $logEntry->setTags( $tags );
2379  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2380  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2381  }
2382  $logId = $logEntry->insert();
2383  $logEntry->publish( $logId );
2384 
2385  return Status::newGood( $logId );
2386  }
2387 
2399  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2400  array $expiry, $cascade, $reason, $user = null
2401  ) {
2402  $dbw = wfGetDB( DB_MASTER );
2403 
2404  // Prepare a null revision to be added to the history
2405  $editComment = wfMessage(
2406  $revCommentMsg,
2407  $this->mTitle->getPrefixedText(),
2408  $user ? $user->getName() : ''
2409  )->inContentLanguage()->text();
2410  if ( $reason ) {
2411  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2412  }
2413  $protectDescription = $this->protectDescription( $limit, $expiry );
2414  if ( $protectDescription ) {
2415  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2416  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2417  ->inContentLanguage()->text();
2418  }
2419  if ( $cascade ) {
2420  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2421  $editComment .= wfMessage( 'brackets' )->params(
2422  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2423  )->inContentLanguage()->text();
2424  }
2425 
2426  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2427  if ( $nullRev ) {
2428  $nullRev->insertOn( $dbw );
2429 
2430  // Update page record and touch page
2431  $oldLatest = $nullRev->getParentId();
2432  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2433  }
2434 
2435  return $nullRev;
2436  }
2437 
2442  protected function formatExpiry( $expiry ) {
2443  if ( $expiry != 'infinity' ) {
2444  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2445  return wfMessage(
2446  'protect-expiring',
2447  $contLang->timeanddate( $expiry, false, false ),
2448  $contLang->date( $expiry, false, false ),
2449  $contLang->time( $expiry, false, false )
2450  )->inContentLanguage()->text();
2451  } else {
2452  return wfMessage( 'protect-expiry-indefinite' )
2453  ->inContentLanguage()->text();
2454  }
2455  }
2456 
2464  public function protectDescription( array $limit, array $expiry ) {
2465  $protectDescription = '';
2466 
2467  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2468  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2469  # All possible message keys are listed here for easier grepping:
2470  # * restriction-create
2471  # * restriction-edit
2472  # * restriction-move
2473  # * restriction-upload
2474  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2475  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2476  # with '' filtered out. All possible message keys are listed below:
2477  # * protect-level-autoconfirmed
2478  # * protect-level-sysop
2479  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2480  ->inContentLanguage()->text();
2481 
2482  $expiryText = $this->formatExpiry( $expiry[$action] );
2483 
2484  if ( $protectDescription !== '' ) {
2485  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2486  }
2487  $protectDescription .= wfMessage( 'protect-summary-desc' )
2488  ->params( $actionText, $restrictionsText, $expiryText )
2489  ->inContentLanguage()->text();
2490  }
2491 
2492  return $protectDescription;
2493  }
2494 
2506  public function protectDescriptionLog( array $limit, array $expiry ) {
2507  $protectDescriptionLog = '';
2508 
2509  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2510  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2511  $expiryText = $this->formatExpiry( $expiry[$action] );
2512  $protectDescriptionLog .=
2513  $dirMark .
2514  "[$action=$restrictions] ($expiryText)";
2515  }
2516 
2517  return trim( $protectDescriptionLog );
2518  }
2519 
2529  protected static function flattenRestrictions( $limit ) {
2530  if ( !is_array( $limit ) ) {
2531  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2532  }
2533 
2534  $bits = [];
2535  ksort( $limit );
2536 
2537  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2538  $bits[] = "$action=$restrictions";
2539  }
2540 
2541  return implode( ':', $bits );
2542  }
2543 
2556  public function isBatchedDelete( $safetyMargin = 0 ) {
2558 
2559  $dbr = wfGetDB( DB_REPLICA );
2560  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2561  $revCount += $safetyMargin;
2562 
2563  return $revCount >= $wgDeleteRevisionsBatchSize;
2564  }
2565 
2585  public function doDeleteArticle(
2586  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2587  $immediate = false
2588  ) {
2589  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2590  [], 'delete', $immediate );
2591 
2592  // Returns true if the page was actually deleted, or is scheduled for deletion
2593  return $status->isOK();
2594  }
2595 
2618  public function doDeleteArticleReal(
2619  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2620  $tags = [], $logsubtype = 'delete', $immediate = false
2621  ) {
2622  global $wgUser;
2623 
2624  wfDebug( __METHOD__ . "\n" );
2625 
2627 
2628  // Avoid PHP 7.1 warning of passing $this by reference
2629  $wikiPage = $this;
2630 
2631  if ( !$deleter ) {
2632  $deleter = $wgUser;
2633  }
2634  if ( !Hooks::run( 'ArticleDelete',
2635  [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2636  ) ) {
2637  if ( $status->isOK() ) {
2638  // Hook aborted but didn't set a fatal status
2639  $status->fatal( 'delete-hook-aborted' );
2640  }
2641  return $status;
2642  }
2643 
2644  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2645  $logsubtype, $immediate );
2646  }
2647 
2656  public function doDeleteArticleBatched(
2657  $reason, $suppress, User $deleter, $tags,
2658  $logsubtype, $immediate = false, $webRequestId = null
2659  ) {
2660  wfDebug( __METHOD__ . "\n" );
2661 
2663 
2664  $dbw = wfGetDB( DB_MASTER );
2665  $dbw->startAtomic( __METHOD__ );
2666 
2667  $this->loadPageData( self::READ_LATEST );
2668  $id = $this->getId();
2669  // T98706: lock the page from various other updates but avoid using
2670  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2671  // the revisions queries (which also JOIN on user). Only lock the page
2672  // row and CAS check on page_latest to see if the trx snapshot matches.
2673  $lockedLatest = $this->lockAndGetLatest();
2674  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2675  $dbw->endAtomic( __METHOD__ );
2676  // Page not there or trx snapshot is stale
2677  $status->error( 'cannotdelete',
2678  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2679  return $status;
2680  }
2681 
2682  // At this point we are now committed to returning an OK
2683  // status unless some DB query error or other exception comes up.
2684  // This way callers don't have to call rollback() if $status is bad
2685  // unless they actually try to catch exceptions (which is rare).
2686 
2687  // we need to remember the old content so we can use it to generate all deletion updates.
2688  $revision = $this->getRevision();
2689  try {
2690  $content = $this->getContent( Revision::RAW );
2691  } catch ( Exception $ex ) {
2692  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2693  . $ex->getMessage() );
2694 
2695  $content = null;
2696  }
2697 
2698  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2699  // one batch of revisions and defer archival of any others to the job queue.
2700  $explictTrxLogged = false;
2701  while ( true ) {
2702  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2703  if ( $done || !$immediate ) {
2704  break;
2705  }
2706  $dbw->endAtomic( __METHOD__ );
2707  if ( $dbw->explicitTrxActive() ) {
2708  // Explict transactions may never happen here in practice. Log to be sure.
2709  if ( !$explictTrxLogged ) {
2710  $explictTrxLogged = true;
2711  LoggerFactory::getInstance( 'wfDebug' )->debug(
2712  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2713  'title' => $this->getTitle()->getText(),
2714  ] );
2715  }
2716  continue;
2717  }
2718  if ( $dbw->trxLevel() ) {
2719  $dbw->commit();
2720  }
2721  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2722  $lbFactory->waitForReplication();
2723  $dbw->startAtomic( __METHOD__ );
2724  }
2725 
2726  // If done archiving, also delete the article.
2727  if ( !$done ) {
2728  $dbw->endAtomic( __METHOD__ );
2729 
2730  $jobParams = [
2731  'namespace' => $this->getTitle()->getNamespace(),
2732  'title' => $this->getTitle()->getDBkey(),
2733  'wikiPageId' => $id,
2734  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2735  'reason' => $reason,
2736  'suppress' => $suppress,
2737  'userId' => $deleter->getId(),
2738  'tags' => json_encode( $tags ),
2739  'logsubtype' => $logsubtype,
2740  ];
2741 
2742  $job = new DeletePageJob( $jobParams );
2743  JobQueueGroup::singleton()->push( $job );
2744 
2745  $status->warning( 'delete-scheduled',
2746  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2747  } else {
2748  // Get archivedRevisionCount by db query, because there's no better alternative.
2749  // Jobs cannot pass a count of archived revisions to the next job, because additional
2750  // deletion operations can be started while the first is running. Jobs from each
2751  // gracefully interleave, but would not know about each other's count. Deduplication
2752  // in the job queue to avoid simultaneous deletion operations would add overhead.
2753  // Number of archived revisions cannot be known beforehand, because edits can be made
2754  // while deletion operations are being processed, changing the number of archivals.
2755  $archivedRevisionCount = (int)$dbw->selectField(
2756  'archive', 'COUNT(*)',
2757  [
2758  'ar_namespace' => $this->getTitle()->getNamespace(),
2759  'ar_title' => $this->getTitle()->getDBkey(),
2760  'ar_page_id' => $id
2761  ], __METHOD__
2762  );
2763 
2764  // Clone the title and wikiPage, so we have the information we need when
2765  // we log and run the ArticleDeleteComplete hook.
2766  $logTitle = clone $this->mTitle;
2767  $wikiPageBeforeDelete = clone $this;
2768 
2769  // Now that it's safely backed up, delete it
2770  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2771 
2772  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2773  $logtype = $suppress ? 'suppress' : 'delete';
2774 
2775  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2776  $logEntry->setPerformer( $deleter );
2777  $logEntry->setTarget( $logTitle );
2778  $logEntry->setComment( $reason );
2779  $logEntry->setTags( $tags );
2780  $logid = $logEntry->insert();
2781 
2782  $dbw->onTransactionPreCommitOrIdle(
2783  function () use ( $logEntry, $logid ) {
2784  // T58776: avoid deadlocks (especially from FileDeleteForm)
2785  $logEntry->publish( $logid );
2786  },
2787  __METHOD__
2788  );
2789 
2790  $dbw->endAtomic( __METHOD__ );
2791 
2792  $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2793 
2794  Hooks::run( 'ArticleDeleteComplete', [
2795  &$wikiPageBeforeDelete,
2796  &$deleter,
2797  $reason,
2798  $id,
2799  $content,
2800  $logEntry,
2801  $archivedRevisionCount
2802  ] );
2803  $status->value = $logid;
2804 
2805  // Show log excerpt on 404 pages rather than just a link
2806  $cache = MediaWikiServices::getInstance()->getMainObjectStash();
2807  $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2808  $cache->set( $key, 1, $cache::TTL_DAY );
2809  }
2810 
2811  return $status;
2812  }
2813 
2823  protected function archiveRevisions( $dbw, $id, $suppress ) {
2826 
2827  // Given the lock above, we can be confident in the title and page ID values
2828  $namespace = $this->getTitle()->getNamespace();
2829  $dbKey = $this->getTitle()->getDBkey();
2830 
2831  $commentStore = CommentStore::getStore();
2832  $actorMigration = ActorMigration::newMigration();
2833 
2835  $bitfield = false;
2836 
2837  // Bitfields to further suppress the content
2838  if ( $suppress ) {
2839  $bitfield = Revision::SUPPRESSED_ALL;
2840  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2841  }
2842 
2843  // For now, shunt the revision data into the archive table.
2844  // Text is *not* removed from the text table; bulk storage
2845  // is left intact to avoid breaking block-compression or
2846  // immutable storage schemes.
2847  // In the future, we may keep revisions and mark them with
2848  // the rev_deleted field, which is reserved for this purpose.
2849 
2850  // Lock rows in `revision` and its temp tables, but not any others.
2851  // Note array_intersect() preserves keys from the first arg, and we're
2852  // assuming $revQuery has `revision` primary and isn't using subtables
2853  // for anything we care about.
2854  $dbw->lockForUpdate(
2855  array_intersect(
2856  $revQuery['tables'],
2857  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2858  ),
2859  [ 'rev_page' => $id ],
2860  __METHOD__,
2861  [],
2862  $revQuery['joins']
2863  );
2864 
2865  // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
2866  // so we can copy it to the archive table.
2867  // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
2868  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2869  $revQuery['fields'][] = 'rev_text_id';
2870 
2871  if ( $wgContentHandlerUseDB ) {
2872  $revQuery['fields'][] = 'rev_content_model';
2873  $revQuery['fields'][] = 'rev_content_format';
2874  }
2875  }
2876 
2877  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2878  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2879  $res = $dbw->select(
2880  $revQuery['tables'],
2881  $revQuery['fields'],
2882  [ 'rev_page' => $id ],
2883  __METHOD__,
2884  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2885  $revQuery['joins']
2886  );
2887 
2888  // Build their equivalent archive rows
2889  $rowsInsert = [];
2890  $revids = [];
2891 
2893  $ipRevIds = [];
2894 
2895  $done = true;
2896  foreach ( $res as $row ) {
2897  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2898  $done = false;
2899  break;
2900  }
2901 
2902  $comment = $commentStore->getComment( 'rev_comment', $row );
2903  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2904  $rowInsert = [
2905  'ar_namespace' => $namespace,
2906  'ar_title' => $dbKey,
2907  'ar_timestamp' => $row->rev_timestamp,
2908  'ar_minor_edit' => $row->rev_minor_edit,
2909  'ar_rev_id' => $row->rev_id,
2910  'ar_parent_id' => $row->rev_parent_id,
2919  'ar_len' => $row->rev_len,
2920  'ar_page_id' => $id,
2921  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2922  'ar_sha1' => $row->rev_sha1,
2923  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2924  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2925 
2926  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2927  $rowInsert['ar_text_id'] = $row->rev_text_id;
2928 
2929  if ( $wgContentHandlerUseDB ) {
2930  $rowInsert['ar_content_model'] = $row->rev_content_model;
2931  $rowInsert['ar_content_format'] = $row->rev_content_format;
2932  }
2933  }
2934 
2935  $rowsInsert[] = $rowInsert;
2936  $revids[] = $row->rev_id;
2937 
2938  // Keep track of IP edits, so that the corresponding rows can
2939  // be deleted in the ip_changes table.
2940  if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2941  $ipRevIds[] = $row->rev_id;
2942  }
2943  }
2944 
2945  // This conditional is just a sanity check
2946  if ( count( $revids ) > 0 ) {
2947  // Copy them into the archive table
2948  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2949 
2950  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2951  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2952  if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
2953  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2954  }
2955 
2956  // Also delete records from ip_changes as applicable.
2957  if ( count( $ipRevIds ) > 0 ) {
2958  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2959  }
2960  }
2961 
2962  return $done;
2963  }
2964 
2971  public function lockAndGetLatest() {
2972  return (int)wfGetDB( DB_MASTER )->selectField(
2973  'page',
2974  'page_latest',
2975  [
2976  'page_id' => $this->getId(),
2977  // Typically page_id is enough, but some code might try to do
2978  // updates assuming the title is the same, so verify that
2979  'page_namespace' => $this->getTitle()->getNamespace(),
2980  'page_title' => $this->getTitle()->getDBkey()
2981  ],
2982  __METHOD__,
2983  [ 'FOR UPDATE' ]
2984  );
2985  }
2986 
2999  public function doDeleteUpdates(
3000  $id, Content $content = null, Revision $revision = null, User $user = null
3001  ) {
3002  if ( $id !== $this->getId() ) {
3003  throw new InvalidArgumentException( 'Mismatching page ID' );
3004  }
3005 
3006  try {
3007  $countable = $this->isCountable();
3008  } catch ( Exception $ex ) {
3009  // fallback for deleting broken pages for which we cannot load the content for
3010  // some reason. Note that doDeleteArticleReal() already logged this problem.
3011  $countable = false;
3012  }
3013 
3014  // Update site status
3016  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3017  ) );
3018 
3019  // Delete pagelinks, update secondary indexes, etc
3020  $updates = $this->getDeletionUpdates(
3021  $revision ? $revision->getRevisionRecord() : $content
3022  );
3023  foreach ( $updates as $update ) {
3024  DeferredUpdates::addUpdate( $update );
3025  }
3026 
3027  $causeAgent = $user ? $user->getName() : 'unknown';
3028  // Reparse any pages transcluding this page
3030  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3031  // Reparse any pages including this image
3032  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3034  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3035  }
3036 
3037  // Clear caches
3038  self::onArticleDelete( $this->mTitle );
3040  $this->mTitle,
3041  $revision,
3042  null,
3044  );
3045 
3046  // Reset this object and the Title object
3047  $this->loadFromRow( false, self::READ_LATEST );
3048 
3049  // Search engine
3050  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3051  }
3052 
3082  public function doRollback(
3083  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3084  ) {
3085  $resultDetails = null;
3086 
3087  // Check permissions
3088  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3089  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3090  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3091 
3092  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3093  $errors[] = [ 'sessionfailure' ];
3094  }
3095 
3096  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3097  $errors[] = [ 'actionthrottledtext' ];
3098  }
3099 
3100  // If there were errors, bail out now
3101  if ( !empty( $errors ) ) {
3102  return $errors;
3103  }
3104 
3105  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3106  }
3107 
3128  public function commitRollback( $fromP, $summary, $bot,
3129  &$resultDetails, User $guser, $tags = null
3130  ) {
3131  global $wgUseRCPatrol;
3132 
3133  $dbw = wfGetDB( DB_MASTER );
3134 
3135  if ( wfReadOnly() ) {
3136  return [ [ 'readonlytext' ] ];
3137  }
3138 
3139  // Begin revision creation cycle by creating a PageUpdater.
3140  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3141  $updater = $this->newPageUpdater( $guser );
3142  $current = $updater->grabParentRevision();
3143 
3144  if ( is_null( $current ) ) {
3145  // Something wrong... no page?
3146  return [ [ 'notanarticle' ] ];
3147  }
3148 
3149  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3150  $legacyCurrent = new Revision( $current );
3151  $from = str_replace( '_', ' ', $fromP );
3152 
3153  // User name given should match up with the top revision.
3154  // If the revision's user is not visible, then $from should be empty.
3155  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3156  $resultDetails = [ 'current' => $legacyCurrent ];
3157  return [ [ 'alreadyrolled',
3158  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3159  htmlspecialchars( $fromP ),
3160  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3161  ] ];
3162  }
3163 
3164  // Get the last edit not by this person...
3165  // Note: these may not be public values
3166  $actorWhere = ActorMigration::newMigration()->getWhere(
3167  $dbw,
3168  'rev_user',
3169  $current->getUser( RevisionRecord::RAW )
3170  );
3171 
3172  $s = $dbw->selectRow(
3173  [ 'revision' ] + $actorWhere['tables'],
3174  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3175  [
3176  'rev_page' => $current->getPageId(),
3177  'NOT(' . $actorWhere['conds'] . ')',
3178  ],
3179  __METHOD__,
3180  [
3181  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3182  'ORDER BY' => 'rev_timestamp DESC'
3183  ],
3184  $actorWhere['joins']
3185  );
3186  if ( $s === false ) {
3187  // No one else ever edited this page
3188  return [ [ 'cantrollback' ] ];
3189  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3190  || $s->rev_deleted & RevisionRecord::DELETED_USER
3191  ) {
3192  // Only admins can see this text
3193  return [ [ 'notvisiblerev' ] ];
3194  }
3195 
3196  // Generate the edit summary if necessary
3197  $target = $this->getRevisionStore()->getRevisionById(
3198  $s->rev_id,
3199  RevisionStore::READ_LATEST
3200  );
3201  if ( empty( $summary ) ) {
3202  if ( !$currentEditorForPublic ) { // no public user name
3203  $summary = wfMessage( 'revertpage-nouser' );
3204  } else {
3205  $summary = wfMessage( 'revertpage' );
3206  }
3207  }
3208  $legacyTarget = new Revision( $target );
3209  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3210 
3211  // Allow the custom summary to use the same args as the default message
3212  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3213  $args = [
3214  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3215  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3216  $s->rev_id,
3217  $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3218  $current->getId(),
3219  $contLang->timeanddate( $current->getTimestamp() )
3220  ];
3221  if ( $summary instanceof Message ) {
3222  $summary = $summary->params( $args )->inContentLanguage()->text();
3223  } else {
3224  $summary = wfMsgReplaceArgs( $summary, $args );
3225  }
3226 
3227  // Trim spaces on user supplied text
3228  $summary = trim( $summary );
3229 
3230  // Save
3231  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3232 
3233  if ( $guser->isAllowed( 'minoredit' ) ) {
3234  $flags |= EDIT_MINOR;
3235  }
3236 
3237  if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3238  $flags |= EDIT_FORCE_BOT;
3239  }
3240 
3241  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3242  $currentContent = $current->getContent( SlotRecord::MAIN );
3243  $targetContent = $target->getContent( SlotRecord::MAIN );
3244  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3245 
3246  if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3247  $tags[] = 'mw-rollback';
3248  }
3249 
3250  // Build rollback revision:
3251  // Restore old content
3252  // TODO: MCR: test this once we can store multiple slots
3253  foreach ( $target->getSlots()->getSlots() as $slot ) {
3254  $updater->inheritSlot( $slot );
3255  }
3256 
3257  // Remove extra slots
3258  // TODO: MCR: test this once we can store multiple slots
3259  foreach ( $current->getSlotRoles() as $role ) {
3260  if ( !$target->hasSlot( $role ) ) {
3261  $updater->removeSlot( $role );
3262  }
3263  }
3264 
3265  $updater->setOriginalRevisionId( $target->getId() );
3266  // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3267  $updater->addTags( $tags );
3268 
3269  // TODO: this logic should not be in the storage layer, it's here for compatibility
3270  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3271  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3272  if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $guser ) ) {
3273  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3274  }
3275 
3276  // Actually store the rollback
3277  $rev = $updater->saveRevision(
3279  $flags
3280  );
3281 
3282  // Set patrolling and bot flag on the edits, which gets rollbacked.
3283  // This is done even on edit failure to have patrolling in that case (T64157).
3284  $set = [];
3285  if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3286  // Mark all reverted edits as bot
3287  $set['rc_bot'] = 1;
3288  }
3289 
3290  if ( $wgUseRCPatrol ) {
3291  // Mark all reverted edits as patrolled
3292  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3293  }
3294 
3295  if ( count( $set ) ) {
3296  $actorWhere = ActorMigration::newMigration()->getWhere(
3297  $dbw,
3298  'rc_user',
3299  $current->getUser( RevisionRecord::RAW ),
3300  false
3301  );
3302  $dbw->update( 'recentchanges', $set,
3303  [ /* WHERE */
3304  'rc_cur_id' => $current->getPageId(),
3305  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3306  $actorWhere['conds'], // No tables/joins are needed for rc_user
3307  ],
3308  __METHOD__
3309  );
3310  }
3311 
3312  if ( !$updater->wasSuccessful() ) {
3313  return $updater->getStatus()->getErrorsArray();
3314  }
3315 
3316  // Report if the edit was not created because it did not change the content.
3317  if ( $updater->isUnchanged() ) {
3318  $resultDetails = [ 'current' => $legacyCurrent ];
3319  return [ [ 'alreadyrolled',
3320  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3321  htmlspecialchars( $fromP ),
3322  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3323  ] ];
3324  }
3325 
3326  if ( $changingContentModel ) {
3327  // If the content model changed during the rollback,
3328  // make sure it gets logged to Special:Log/contentmodel
3329  $log = new ManualLogEntry( 'contentmodel', 'change' );
3330  $log->setPerformer( $guser );
3331  $log->setTarget( $this->mTitle );
3332  $log->setComment( $summary );
3333  $log->setParameters( [
3334  '4::oldmodel' => $currentContent->getModel(),
3335  '5::newmodel' => $targetContent->getModel(),
3336  ] );
3337 
3338  $logId = $log->insert( $dbw );
3339  $log->publish( $logId );
3340  }
3341 
3342  $revId = $rev->getId();
3343 
3344  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3345 
3346  $resultDetails = [
3347  'summary' => $summary,
3348  'current' => $legacyCurrent,
3349  'target' => $legacyTarget,
3350  'newid' => $revId,
3351  'tags' => $tags
3352  ];
3353 
3354  // TODO: make this return a Status object and wrap $resultDetails in that.
3355  return [];
3356  }
3357 
3369  public static function onArticleCreate( Title $title ) {
3370  // TODO: move this into a PageEventEmitter service
3371 
3372  // Update existence markers on article/talk tabs...
3373  $other = $title->getOtherPage();
3374 
3375  $other->purgeSquid();
3376 
3377  $title->touchLinks();
3378  $title->purgeSquid();
3379  $title->deleteTitleProtection();
3380 
3381  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3382 
3383  // Invalidate caches of articles which include this page
3385  new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
3386  );
3387 
3388  if ( $title->getNamespace() == NS_CATEGORY ) {
3389  // Load the Category object, which will schedule a job to create
3390  // the category table row if necessary. Checking a replica DB is ok
3391  // here, in the worst case it'll run an unnecessary recount job on
3392  // a category that probably doesn't have many members.
3393  Category::newFromTitle( $title )->getID();
3394  }
3395  }
3396 
3402  public static function onArticleDelete( Title $title ) {
3403  // TODO: move this into a PageEventEmitter service
3404 
3405  // Update existence markers on article/talk tabs...
3406  // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3407  BacklinkCache::get( $title )->clear();
3408  $other = $title->getOtherPage();
3409 
3410  $other->purgeSquid();
3411 
3412  $title->touchLinks();
3413  $title->purgeSquid();
3414 
3415  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3416 
3417  // File cache
3419  InfoAction::invalidateCache( $title );
3420 
3421  // Messages
3422  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3423  MessageCache::singleton()->updateMessageOverride( $title, null );
3424  }
3425 
3426  // Images
3427  if ( $title->getNamespace() == NS_FILE ) {
3429  new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
3430  );
3431  }
3432 
3433  // User talk pages
3434  if ( $title->getNamespace() == NS_USER_TALK ) {
3435  $user = User::newFromName( $title->getText(), false );
3436  if ( $user ) {
3437  $user->setNewtalk( false );
3438  }
3439  }
3440 
3441  // Image redirects
3442  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3443 
3444  // Purge cross-wiki cache entities referencing this page
3445  self::purgeInterwikiCheckKey( $title );
3446  }
3447 
3456  public static function onArticleEdit(
3457  Title $title,
3458  Revision $revision = null,
3459  $slotsChanged = null
3460  ) {
3461  // TODO: move this into a PageEventEmitter service
3462 
3463  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3464  // Invalidate caches of articles which include this page.
3465  // Only for the main slot, because only the main slot is transcluded.
3466  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3468  new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
3469  );
3470  }
3471 
3472  // Invalidate the caches of all pages which redirect here
3474  new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
3475  );
3476 
3477  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3478 
3479  // Purge CDN for this page only
3480  $title->purgeSquid();
3481  // Clear file cache for this page only
3483 
3484  // Purge ?action=info cache
3485  $revid = $revision ? $revision->getId() : null;
3486  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3487  InfoAction::invalidateCache( $title, $revid );
3488  } );
3489 
3490  // Purge cross-wiki cache entities referencing this page
3491  self::purgeInterwikiCheckKey( $title );
3492  }
3493 
3501  private static function purgeInterwikiCheckKey( Title $title ) {
3503 
3504  if ( !$wgEnableScaryTranscluding ) {
3505  return; // @todo: perhaps this wiki is only used as a *source* for content?
3506  }
3507 
3508  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3509  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3510  $cache->resetCheckKey(
3511  // Do not include the namespace since there can be multiple aliases to it
3512  // due to different namespace text definitions on different wikis. This only
3513  // means that some cache invalidations happen that are not strictly needed.
3514  $cache->makeGlobalKey(
3515  'interwiki-page',
3517  $title->getDBkey()
3518  )
3519  );
3520  } );
3521  }
3522 
3529  public function getCategories() {
3530  $id = $this->getId();
3531  if ( $id == 0 ) {
3532  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3533  }
3534 
3535  $dbr = wfGetDB( DB_REPLICA );
3536  $res = $dbr->select( 'categorylinks',
3537  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3538  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3539  // as not being aliases, and NS_CATEGORY is numeric
3540  [ 'cl_from' => $id ],
3541  __METHOD__ );
3542 
3543  return TitleArray::newFromResult( $res );
3544  }
3545 
3552  public function getHiddenCategories() {
3553  $result = [];
3554  $id = $this->getId();
3555 
3556  if ( $id == 0 ) {
3557  return [];
3558  }
3559 
3560  $dbr = wfGetDB( DB_REPLICA );
3561  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3562  [ 'cl_to' ],
3563  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3564  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3565  __METHOD__ );
3566 
3567  if ( $res !== false ) {
3568  foreach ( $res as $row ) {
3569  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3570  }
3571  }
3572 
3573  return $result;
3574  }
3575 
3583  public function getAutoDeleteReason( &$hasHistory ) {
3584  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3585  }
3586 
3597  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3598  $id = $id ?: $this->getId();
3599  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
3600  getCategoryLinkType( $this->getTitle()->getNamespace() );
3601 
3602  $addFields = [ 'cat_pages = cat_pages + 1' ];
3603  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3604  if ( $type !== 'page' ) {
3605  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3606  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3607  }
3608 
3609  $dbw = wfGetDB( DB_MASTER );
3610 
3611  if ( count( $added ) ) {
3612  $existingAdded = $dbw->selectFieldValues(
3613  'category',
3614  'cat_title',
3615  [ 'cat_title' => $added ],
3616  __METHOD__
3617  );
3618 
3619  // For category rows that already exist, do a plain
3620  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3621  // to avoid creating gaps in the cat_id sequence.
3622  if ( count( $existingAdded ) ) {
3623  $dbw->update(
3624  'category',
3625  $addFields,
3626  [ 'cat_title' => $existingAdded ],
3627  __METHOD__
3628  );
3629  }
3630 
3631  $missingAdded = array_diff( $added, $existingAdded );
3632  if ( count( $missingAdded ) ) {
3633  $insertRows = [];
3634  foreach ( $missingAdded as $cat ) {
3635  $insertRows[] = [
3636  'cat_title' => $cat,
3637  'cat_pages' => 1,
3638  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3639  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3640  ];
3641  }
3642  $dbw->upsert(
3643  'category',
3644  $insertRows,
3645  [ 'cat_title' ],
3646  $addFields,
3647  __METHOD__
3648  );
3649  }
3650  }
3651 
3652  if ( count( $deleted ) ) {
3653  $dbw->update(
3654  'category',
3655  $removeFields,
3656  [ 'cat_title' => $deleted ],
3657  __METHOD__
3658  );
3659  }
3660 
3661  foreach ( $added as $catName ) {
3662  $cat = Category::newFromName( $catName );
3663  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3664  }
3665 
3666  foreach ( $deleted as $catName ) {
3667  $cat = Category::newFromName( $catName );
3668  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3669  // Refresh counts on categories that should be empty now (after commit, T166757)
3670  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3671  $cat->refreshCountsIfEmpty();
3672  } );
3673  }
3674  }
3675 
3682  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3683  if ( wfReadOnly() ) {
3684  return;
3685  }
3686 
3687  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3688  [ $this, $this->mTitle, $parserOutput ]
3689  ) ) {
3690  return;
3691  }
3692 
3693  $config = RequestContext::getMain()->getConfig();
3694 
3695  $params = [
3696  'isOpportunistic' => true,
3697  'rootJobTimestamp' => $parserOutput->getCacheTime()
3698  ];
3699 
3700  if ( $this->mTitle->areRestrictionsCascading() ) {
3701  // If the page is cascade protecting, the links should really be up-to-date
3702  JobQueueGroup::singleton()->lazyPush(
3703  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3704  );
3705  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3706  // Assume the output contains "dynamic" time/random based magic words.
3707  // Only update pages that expired due to dynamic content and NOT due to edits
3708  // to referenced templates/files. When the cache expires due to dynamic content,
3709  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3710  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3711  // template/file edit already triggered recursive RefreshLinksJob jobs.
3712  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3713  // If a page is uncacheable, do not keep spamming a job for it.
3714  // Although it would be de-duplicated, it would still waste I/O.
3716  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3717  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3718  if ( $cache->add( $key, time(), $ttl ) ) {
3719  JobQueueGroup::singleton()->lazyPush(
3720  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3721  );
3722  }
3723  }
3724  }
3725  }
3726 
3736  public function getDeletionUpdates( $rev = null ) {
3737  if ( !$rev ) {
3738  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3739 
3740  try {
3741  $rev = $this->getRevisionRecord();
3742  } catch ( Exception $ex ) {
3743  // If we can't load the content, something is wrong. Perhaps that's why
3744  // the user is trying to delete the page, so let's not fail in that case.
3745  // Note that doDeleteArticleReal() will already have logged an issue with
3746  // loading the content.
3747  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3748  }
3749  }
3750 
3751  if ( !$rev ) {
3752  $slotContent = [];
3753  } elseif ( $rev instanceof Content ) {
3754  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3755 
3756  $slotContent = [ SlotRecord::MAIN => $rev ];
3757  } else {
3758  $slotContent = array_map( function ( SlotRecord $slot ) {
3759  return $slot->getContent( Revision::RAW );
3760  }, $rev->getSlots()->getSlots() );
3761  }
3762 
3763  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3764 
3765  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3766  // model here, not the content object!
3767  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3769  foreach ( $slotContent as $role => $content ) {
3770  $handler = $content->getContentHandler();
3771 
3772  $updates = $handler->getDeletionUpdates(
3773  $this->getTitle(),
3774  $role
3775  );
3776  $allUpdates = array_merge( $allUpdates, $updates );
3777 
3778  // TODO: remove B/C hack in 1.32!
3779  $legacyUpdates = $content->getDeletionUpdates( $this );
3780 
3781  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3782  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3783  return !( $update instanceof LinksDeletionUpdate );
3784  } );
3785 
3786  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3787  }
3788 
3789  Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3790 
3791  // TODO: hard deprecate old hook in 1.33
3792  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3793  return $allUpdates;
3794  }
3795 
3803  public function isLocal() {
3804  return true;
3805  }
3806 
3816  public function getWikiDisplayName() {
3817  global $wgSitename;
3818  return $wgSitename;
3819  }
3820 
3829  public function getSourceURL() {
3830  return $this->getTitle()->getCanonicalURL();
3831  }
3832 
3839  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3840 
3841  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3842  }
3843 
3844 }
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:816
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:699
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:284
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:138
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:2852
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2072
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:3369
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4233
$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:1585
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:3816
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:984
clearNotification(&$title, $oldid=0)
Clear the user&#39;s notification timestamp for the given title.
Definition: User.php:3842
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:1982
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:3597
getTimestamp()
Definition: Revision.php:994
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3700
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2464
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:3501
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:159
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)
$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:3682
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3829
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:1564
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2656
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:154
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
const EDIT_UPDATE
Definition: Defines.php:153
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:519
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:3838
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:979
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2311
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
static newFromAnyId( $userId, $userName, $actorId, $wikiId=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:681
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. '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:1980
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:3456
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:50
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:780
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:3128
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:2618
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:1263
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:1789
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:70
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:129
wfReadOnly()
Check whether the wiki is in read-only mode.
wfIncrStats( $key, $count=1)
Increment a statistics counter.
static newMigration()
Static constructor.
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
deleteTitleProtection()
Remove any title protection due to page existing.
Definition: Title.php:2405
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:2556
const FOR_PUBLIC
Definition: Revision.php:54
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
const EDIT_FORCE_BOT
Definition: Defines.php:156
static clearFileCache(Title $title)
Clear the file caches for a page for all actions.
doDeleteArticle( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $user=null, $immediate=false)
Same as doDeleteArticleReal(), but returns a simple boolean.
Definition: WikiPage.php:2585
doDeleteUpdates( $id, Content $content=null, Revision $revision=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:2999
Revision $mLastRevision
Definition: WikiPage.php:92
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:1002
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:286
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:52
$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:55
__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:3552
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:78
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3730
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:1982
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:57
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
static newFromResult( $res)
Definition: TitleArray.php:40
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
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:1026
const NS_FILE
Definition: Defines.php:70
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1766
int false $mLatest
False means "not loaded".
Definition: WikiPage.php:69
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:912
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:556
const RAW
Definition: Revision.php:56
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:656
Special handling for file pages.
const NS_MEDIAWIKI
Definition: Defines.php:72
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php: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:2113
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3583
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2399
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:1945
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:589
static getCurrentWikiDbDomain()
Definition: WikiMap.php:302
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
getComment( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:911
getUser( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:854
static newDynamic(Title $title, array $params)
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:613
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 ...
getUserText( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:892
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:3529
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:2044
getId()
Get the user&#39;s ID.
Definition: User.php:2284
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4542
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1987
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3803
const EDIT_NEW
Definition: Defines.php:152
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:1733
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
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:3736
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3402
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:3082
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
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:2633
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:995
formatExpiry( $expiry)
Definition: WikiPage.php:2442
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:2529
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:2971
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:1870
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:589
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:67
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:1975
$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
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
Definition: WikiPage.php:2823
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:3414
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1473
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:2143
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
const SUPPRESSED_ALL
Definition: Revision.php:51
getCreator( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:873
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2506