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 
1977  public function prepareContentForEdit(
1978  Content $content,
1979  $revision = null,
1980  User $user = null,
1981  $serialFormat = null,
1982  $useCache = true
1983  ) {
1984  global $wgUser;
1985 
1986  if ( !$user ) {
1987  $user = $wgUser;
1988  }
1989 
1990  if ( !is_object( $revision ) ) {
1991  $revid = $revision;
1992  // This code path is deprecated, and nothing is known to
1993  // use it, so performance here shouldn't be a worry.
1994  if ( $revid !== null ) {
1995  wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
1996  $store = $this->getRevisionStore();
1997  $revision = $store->getRevisionById( $revid, Revision::READ_LATEST );
1998  } else {
1999  $revision = null;
2000  }
2001  } elseif ( $revision instanceof Revision ) {
2002  $revision = $revision->getRevisionRecord();
2003  }
2004 
2005  $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2006  $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2007 
2008  if ( !$updater->isUpdatePrepared() ) {
2009  $updater->prepareContent( $user, $slots, $useCache );
2010 
2011  if ( $revision ) {
2012  $updater->prepareUpdate(
2013  $revision,
2014  [
2015  'causeAction' => 'prepare-edit',
2016  'causeAgent' => $user->getName(),
2017  ]
2018  );
2019  }
2020  }
2021 
2022  return $updater->getPreparedEdit();
2023  }
2024 
2052  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2053  $options += [
2054  'causeAction' => 'edit-page',
2055  'causeAgent' => $user->getName(),
2056  ];
2057 
2058  $revision = $revision->getRevisionRecord();
2059 
2060  $updater = $this->getDerivedDataUpdater( $user, $revision );
2061 
2062  $updater->prepareUpdate( $revision, $options );
2063 
2064  $updater->doUpdates();
2065  }
2066 
2080  public function updateParserCache( array $options = [] ) {
2081  $revision = $this->getRevisionRecord();
2082  if ( !$revision || !$revision->getId() ) {
2083  LoggerFactory::getInstance( 'wikipage' )->info(
2084  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2085  );
2086  return;
2087  }
2088  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2089 
2090  $updater = $this->getDerivedDataUpdater( $user, $revision );
2091  $updater->prepareUpdate( $revision, $options );
2092  $updater->doParserCacheUpdate();
2093  }
2094 
2121  public function doSecondaryDataUpdates( array $options = [] ) {
2122  $options['recursive'] = $options['recursive'] ?? true;
2123  $revision = $this->getRevisionRecord();
2124  if ( !$revision || !$revision->getId() ) {
2125  LoggerFactory::getInstance( 'wikipage' )->info(
2126  __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2127  );
2128  return;
2129  }
2130  $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2131 
2132  $updater = $this->getDerivedDataUpdater( $user, $revision );
2133  $updater->prepareUpdate( $revision, $options );
2134  $updater->doSecondaryDataUpdates( $options );
2135  }
2136 
2151  public function doUpdateRestrictions( array $limit, array $expiry,
2152  &$cascade, $reason, User $user, $tags = null
2153  ) {
2155 
2156  if ( wfReadOnly() ) {
2157  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2158  }
2159 
2160  $this->loadPageData( 'fromdbmaster' );
2161  $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2162  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2163  $id = $this->getId();
2164 
2165  if ( !$cascade ) {
2166  $cascade = false;
2167  }
2168 
2169  // Take this opportunity to purge out expired restrictions
2171 
2172  // @todo: Same limitations as described in ProtectionForm.php (line 37);
2173  // we expect a single selection, but the schema allows otherwise.
2174  $isProtected = false;
2175  $protect = false;
2176  $changed = false;
2177 
2178  $dbw = wfGetDB( DB_MASTER );
2179 
2180  foreach ( $restrictionTypes as $action ) {
2181  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2182  $expiry[$action] = 'infinity';
2183  }
2184  if ( !isset( $limit[$action] ) ) {
2185  $limit[$action] = '';
2186  } elseif ( $limit[$action] != '' ) {
2187  $protect = true;
2188  }
2189 
2190  // Get current restrictions on $action
2191  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2192  if ( $current != '' ) {
2193  $isProtected = true;
2194  }
2195 
2196  if ( $limit[$action] != $current ) {
2197  $changed = true;
2198  } elseif ( $limit[$action] != '' ) {
2199  // Only check expiry change if the action is actually being
2200  // protected, since expiry does nothing on an not-protected
2201  // action.
2202  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2203  $changed = true;
2204  }
2205  }
2206  }
2207 
2208  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2209  $changed = true;
2210  }
2211 
2212  // If nothing has changed, do nothing
2213  if ( !$changed ) {
2214  return Status::newGood();
2215  }
2216 
2217  if ( !$protect ) { // No protection at all means unprotection
2218  $revCommentMsg = 'unprotectedarticle-comment';
2219  $logAction = 'unprotect';
2220  } elseif ( $isProtected ) {
2221  $revCommentMsg = 'modifiedarticleprotection-comment';
2222  $logAction = 'modify';
2223  } else {
2224  $revCommentMsg = 'protectedarticle-comment';
2225  $logAction = 'protect';
2226  }
2227 
2228  $logRelationsValues = [];
2229  $logRelationsField = null;
2230  $logParamsDetails = [];
2231 
2232  // Null revision (used for change tag insertion)
2233  $nullRevision = null;
2234 
2235  if ( $id ) { // Protection of existing page
2236  // Avoid PHP 7.1 warning of passing $this by reference
2237  $wikiPage = $this;
2238 
2239  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2240  return Status::newGood();
2241  }
2242 
2243  // Only certain restrictions can cascade...
2244  $editrestriction = isset( $limit['edit'] )
2245  ? [ $limit['edit'] ]
2246  : $this->mTitle->getRestrictions( 'edit' );
2247  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2248  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2249  }
2250  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2251  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2252  }
2253 
2254  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2255  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2256  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2257  }
2258  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2259  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2260  }
2261 
2262  // The schema allows multiple restrictions
2263  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2264  $cascade = false;
2265  }
2266 
2267  // insert null revision to identify the page protection change as edit summary
2268  $latest = $this->getLatest();
2269  $nullRevision = $this->insertProtectNullRevision(
2270  $revCommentMsg,
2271  $limit,
2272  $expiry,
2273  $cascade,
2274  $reason,
2275  $user
2276  );
2277 
2278  if ( $nullRevision === null ) {
2279  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2280  }
2281 
2282  $logRelationsField = 'pr_id';
2283 
2284  // Update restrictions table
2285  foreach ( $limit as $action => $restrictions ) {
2286  $dbw->delete(
2287  'page_restrictions',
2288  [
2289  'pr_page' => $id,
2290  'pr_type' => $action
2291  ],
2292  __METHOD__
2293  );
2294  if ( $restrictions != '' ) {
2295  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2296  $dbw->insert(
2297  'page_restrictions',
2298  [
2299  'pr_page' => $id,
2300  'pr_type' => $action,
2301  'pr_level' => $restrictions,
2302  'pr_cascade' => $cascadeValue,
2303  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2304  ],
2305  __METHOD__
2306  );
2307  $logRelationsValues[] = $dbw->insertId();
2308  $logParamsDetails[] = [
2309  'type' => $action,
2310  'level' => $restrictions,
2311  'expiry' => $expiry[$action],
2312  'cascade' => (bool)$cascadeValue,
2313  ];
2314  }
2315  }
2316 
2317  // Clear out legacy restriction fields
2318  $dbw->update(
2319  'page',
2320  [ 'page_restrictions' => '' ],
2321  [ 'page_id' => $id ],
2322  __METHOD__
2323  );
2324 
2325  // Avoid PHP 7.1 warning of passing $this by reference
2326  $wikiPage = $this;
2327 
2328  Hooks::run( 'NewRevisionFromEditComplete',
2329  [ $this, $nullRevision, $latest, $user ] );
2330  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2331  } else { // Protection of non-existing page (also known as "title protection")
2332  // Cascade protection is meaningless in this case
2333  $cascade = false;
2334 
2335  if ( $limit['create'] != '' ) {
2336  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2337  $dbw->replace( 'protected_titles',
2338  [ [ 'pt_namespace', 'pt_title' ] ],
2339  [
2340  'pt_namespace' => $this->mTitle->getNamespace(),
2341  'pt_title' => $this->mTitle->getDBkey(),
2342  'pt_create_perm' => $limit['create'],
2343  'pt_timestamp' => $dbw->timestamp(),
2344  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2345  'pt_user' => $user->getId(),
2346  ] + $commentFields, __METHOD__
2347  );
2348  $logParamsDetails[] = [
2349  'type' => 'create',
2350  'level' => $limit['create'],
2351  'expiry' => $expiry['create'],
2352  ];
2353  } else {
2354  $dbw->delete( 'protected_titles',
2355  [
2356  'pt_namespace' => $this->mTitle->getNamespace(),
2357  'pt_title' => $this->mTitle->getDBkey()
2358  ], __METHOD__
2359  );
2360  }
2361  }
2362 
2363  $this->mTitle->flushRestrictions();
2364  InfoAction::invalidateCache( $this->mTitle );
2365 
2366  if ( $logAction == 'unprotect' ) {
2367  $params = [];
2368  } else {
2369  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2370  $params = [
2371  '4::description' => $protectDescriptionLog, // parameter for IRC
2372  '5:bool:cascade' => $cascade,
2373  'details' => $logParamsDetails, // parameter for localize and api
2374  ];
2375  }
2376 
2377  // Update the protection log
2378  $logEntry = new ManualLogEntry( 'protect', $logAction );
2379  $logEntry->setTarget( $this->mTitle );
2380  $logEntry->setComment( $reason );
2381  $logEntry->setPerformer( $user );
2382  $logEntry->setParameters( $params );
2383  if ( !is_null( $nullRevision ) ) {
2384  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2385  }
2386  $logEntry->setTags( $tags );
2387  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2388  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2389  }
2390  $logId = $logEntry->insert();
2391  $logEntry->publish( $logId );
2392 
2393  return Status::newGood( $logId );
2394  }
2395 
2407  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2408  array $expiry, $cascade, $reason, $user = null
2409  ) {
2410  $dbw = wfGetDB( DB_MASTER );
2411 
2412  // Prepare a null revision to be added to the history
2413  $editComment = wfMessage(
2414  $revCommentMsg,
2415  $this->mTitle->getPrefixedText(),
2416  $user ? $user->getName() : ''
2417  )->inContentLanguage()->text();
2418  if ( $reason ) {
2419  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2420  }
2421  $protectDescription = $this->protectDescription( $limit, $expiry );
2422  if ( $protectDescription ) {
2423  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2424  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2425  ->inContentLanguage()->text();
2426  }
2427  if ( $cascade ) {
2428  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2429  $editComment .= wfMessage( 'brackets' )->params(
2430  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2431  )->inContentLanguage()->text();
2432  }
2433 
2434  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2435  if ( $nullRev ) {
2436  $nullRev->insertOn( $dbw );
2437 
2438  // Update page record and touch page
2439  $oldLatest = $nullRev->getParentId();
2440  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2441  }
2442 
2443  return $nullRev;
2444  }
2445 
2450  protected function formatExpiry( $expiry ) {
2451  if ( $expiry != 'infinity' ) {
2452  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2453  return wfMessage(
2454  'protect-expiring',
2455  $contLang->timeanddate( $expiry, false, false ),
2456  $contLang->date( $expiry, false, false ),
2457  $contLang->time( $expiry, false, false )
2458  )->inContentLanguage()->text();
2459  } else {
2460  return wfMessage( 'protect-expiry-indefinite' )
2461  ->inContentLanguage()->text();
2462  }
2463  }
2464 
2472  public function protectDescription( array $limit, array $expiry ) {
2473  $protectDescription = '';
2474 
2475  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2476  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2477  # All possible message keys are listed here for easier grepping:
2478  # * restriction-create
2479  # * restriction-edit
2480  # * restriction-move
2481  # * restriction-upload
2482  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2483  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2484  # with '' filtered out. All possible message keys are listed below:
2485  # * protect-level-autoconfirmed
2486  # * protect-level-sysop
2487  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2488  ->inContentLanguage()->text();
2489 
2490  $expiryText = $this->formatExpiry( $expiry[$action] );
2491 
2492  if ( $protectDescription !== '' ) {
2493  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2494  }
2495  $protectDescription .= wfMessage( 'protect-summary-desc' )
2496  ->params( $actionText, $restrictionsText, $expiryText )
2497  ->inContentLanguage()->text();
2498  }
2499 
2500  return $protectDescription;
2501  }
2502 
2514  public function protectDescriptionLog( array $limit, array $expiry ) {
2515  $protectDescriptionLog = '';
2516 
2517  $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2518  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2519  $expiryText = $this->formatExpiry( $expiry[$action] );
2520  $protectDescriptionLog .=
2521  $dirMark .
2522  "[$action=$restrictions] ($expiryText)";
2523  }
2524 
2525  return trim( $protectDescriptionLog );
2526  }
2527 
2537  protected static function flattenRestrictions( $limit ) {
2538  if ( !is_array( $limit ) ) {
2539  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2540  }
2541 
2542  $bits = [];
2543  ksort( $limit );
2544 
2545  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2546  $bits[] = "$action=$restrictions";
2547  }
2548 
2549  return implode( ':', $bits );
2550  }
2551 
2564  public function isBatchedDelete( $safetyMargin = 0 ) {
2566 
2567  $dbr = wfGetDB( DB_REPLICA );
2568  $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2569  $revCount += $safetyMargin;
2570 
2571  return $revCount >= $wgDeleteRevisionsBatchSize;
2572  }
2573 
2593  public function doDeleteArticle(
2594  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2595  $immediate = false
2596  ) {
2597  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2598  [], 'delete', $immediate );
2599 
2600  // Returns true if the page was actually deleted, or is scheduled for deletion
2601  return $status->isOK();
2602  }
2603 
2626  public function doDeleteArticleReal(
2627  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2628  $tags = [], $logsubtype = 'delete', $immediate = false
2629  ) {
2630  global $wgUser;
2631 
2632  wfDebug( __METHOD__ . "\n" );
2633 
2635 
2636  // Avoid PHP 7.1 warning of passing $this by reference
2637  $wikiPage = $this;
2638 
2639  if ( !$deleter ) {
2640  $deleter = $wgUser;
2641  }
2642  if ( !Hooks::run( 'ArticleDelete',
2643  [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2644  ) ) {
2645  if ( $status->isOK() ) {
2646  // Hook aborted but didn't set a fatal status
2647  $status->fatal( 'delete-hook-aborted' );
2648  }
2649  return $status;
2650  }
2651 
2652  return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2653  $logsubtype, $immediate );
2654  }
2655 
2664  public function doDeleteArticleBatched(
2665  $reason, $suppress, User $deleter, $tags,
2666  $logsubtype, $immediate = false, $webRequestId = null
2667  ) {
2668  wfDebug( __METHOD__ . "\n" );
2669 
2671 
2672  $dbw = wfGetDB( DB_MASTER );
2673  $dbw->startAtomic( __METHOD__ );
2674 
2675  $this->loadPageData( self::READ_LATEST );
2676  $id = $this->getId();
2677  // T98706: lock the page from various other updates but avoid using
2678  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2679  // the revisions queries (which also JOIN on user). Only lock the page
2680  // row and CAS check on page_latest to see if the trx snapshot matches.
2681  $lockedLatest = $this->lockAndGetLatest();
2682  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2683  $dbw->endAtomic( __METHOD__ );
2684  // Page not there or trx snapshot is stale
2685  $status->error( 'cannotdelete',
2686  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2687  return $status;
2688  }
2689 
2690  // At this point we are now committed to returning an OK
2691  // status unless some DB query error or other exception comes up.
2692  // This way callers don't have to call rollback() if $status is bad
2693  // unless they actually try to catch exceptions (which is rare).
2694 
2695  // we need to remember the old content so we can use it to generate all deletion updates.
2696  $revision = $this->getRevision();
2697  try {
2698  $content = $this->getContent( Revision::RAW );
2699  } catch ( Exception $ex ) {
2700  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2701  . $ex->getMessage() );
2702 
2703  $content = null;
2704  }
2705 
2706  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2707  // one batch of revisions and defer archival of any others to the job queue.
2708  $explictTrxLogged = false;
2709  while ( true ) {
2710  $done = $this->archiveRevisions( $dbw, $id, $suppress );
2711  if ( $done || !$immediate ) {
2712  break;
2713  }
2714  $dbw->endAtomic( __METHOD__ );
2715  if ( $dbw->explicitTrxActive() ) {
2716  // Explict transactions may never happen here in practice. Log to be sure.
2717  if ( !$explictTrxLogged ) {
2718  $explictTrxLogged = true;
2719  LoggerFactory::getInstance( 'wfDebug' )->debug(
2720  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2721  'title' => $this->getTitle()->getText(),
2722  ] );
2723  }
2724  continue;
2725  }
2726  if ( $dbw->trxLevel() ) {
2727  $dbw->commit();
2728  }
2729  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2730  $lbFactory->waitForReplication();
2731  $dbw->startAtomic( __METHOD__ );
2732  }
2733 
2734  // If done archiving, also delete the article.
2735  if ( !$done ) {
2736  $dbw->endAtomic( __METHOD__ );
2737 
2738  $jobParams = [
2739  'wikiPageId' => $id,
2740  'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2741  'reason' => $reason,
2742  'suppress' => $suppress,
2743  'userId' => $deleter->getId(),
2744  'tags' => json_encode( $tags ),
2745  'logsubtype' => $logsubtype,
2746  ];
2747 
2748  $job = new DeletePageJob( $this->getTitle(), $jobParams );
2749  JobQueueGroup::singleton()->push( $job );
2750 
2751  $status->warning( 'delete-scheduled',
2752  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2753  } else {
2754  // Get archivedRevisionCount by db query, because there's no better alternative.
2755  // Jobs cannot pass a count of archived revisions to the next job, because additional
2756  // deletion operations can be started while the first is running. Jobs from each
2757  // gracefully interleave, but would not know about each other's count. Deduplication
2758  // in the job queue to avoid simultaneous deletion operations would add overhead.
2759  // Number of archived revisions cannot be known beforehand, because edits can be made
2760  // while deletion operations are being processed, changing the number of archivals.
2761  $archivedRevisionCount = (int)$dbw->selectField(
2762  'archive', 'COUNT(*)',
2763  [
2764  'ar_namespace' => $this->getTitle()->getNamespace(),
2765  'ar_title' => $this->getTitle()->getDBkey(),
2766  'ar_page_id' => $id
2767  ], __METHOD__
2768  );
2769 
2770  // Clone the title and wikiPage, so we have the information we need when
2771  // we log and run the ArticleDeleteComplete hook.
2772  $logTitle = clone $this->mTitle;
2773  $wikiPageBeforeDelete = clone $this;
2774 
2775  // Now that it's safely backed up, delete it
2776  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2777 
2778  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2779  $logtype = $suppress ? 'suppress' : 'delete';
2780 
2781  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2782  $logEntry->setPerformer( $deleter );
2783  $logEntry->setTarget( $logTitle );
2784  $logEntry->setComment( $reason );
2785  $logEntry->setTags( $tags );
2786  $logid = $logEntry->insert();
2787 
2788  $dbw->onTransactionPreCommitOrIdle(
2789  function () use ( $logEntry, $logid ) {
2790  // T58776: avoid deadlocks (especially from FileDeleteForm)
2791  $logEntry->publish( $logid );
2792  },
2793  __METHOD__
2794  );
2795 
2796  $dbw->endAtomic( __METHOD__ );
2797 
2798  $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2799 
2800  Hooks::run( 'ArticleDeleteComplete', [
2801  &$wikiPageBeforeDelete,
2802  &$deleter,
2803  $reason,
2804  $id,
2805  $content,
2806  $logEntry,
2807  $archivedRevisionCount
2808  ] );
2809  $status->value = $logid;
2810 
2811  // Show log excerpt on 404 pages rather than just a link
2812  $cache = MediaWikiServices::getInstance()->getMainObjectStash();
2813  $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2814  $cache->set( $key, 1, $cache::TTL_DAY );
2815  }
2816 
2817  return $status;
2818  }
2819 
2829  protected function archiveRevisions( $dbw, $id, $suppress ) {
2832 
2833  // Given the lock above, we can be confident in the title and page ID values
2834  $namespace = $this->getTitle()->getNamespace();
2835  $dbKey = $this->getTitle()->getDBkey();
2836 
2837  $commentStore = CommentStore::getStore();
2838  $actorMigration = ActorMigration::newMigration();
2839 
2841  $bitfield = false;
2842 
2843  // Bitfields to further suppress the content
2844  if ( $suppress ) {
2845  $bitfield = Revision::SUPPRESSED_ALL;
2846  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2847  }
2848 
2849  // For now, shunt the revision data into the archive table.
2850  // Text is *not* removed from the text table; bulk storage
2851  // is left intact to avoid breaking block-compression or
2852  // immutable storage schemes.
2853  // In the future, we may keep revisions and mark them with
2854  // the rev_deleted field, which is reserved for this purpose.
2855 
2856  // Lock rows in `revision` and its temp tables, but not any others.
2857  // Note array_intersect() preserves keys from the first arg, and we're
2858  // assuming $revQuery has `revision` primary and isn't using subtables
2859  // for anything we care about.
2860  $dbw->lockForUpdate(
2861  array_intersect(
2862  $revQuery['tables'],
2863  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2864  ),
2865  [ 'rev_page' => $id ],
2866  __METHOD__,
2867  [],
2868  $revQuery['joins']
2869  );
2870 
2871  // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
2872  // so we can copy it to the archive table.
2873  // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
2874  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2875  $revQuery['fields'][] = 'rev_text_id';
2876 
2877  if ( $wgContentHandlerUseDB ) {
2878  $revQuery['fields'][] = 'rev_content_model';
2879  $revQuery['fields'][] = 'rev_content_format';
2880  }
2881  }
2882 
2883  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2884  // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2885  $res = $dbw->select(
2886  $revQuery['tables'],
2887  $revQuery['fields'],
2888  [ 'rev_page' => $id ],
2889  __METHOD__,
2890  [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2891  $revQuery['joins']
2892  );
2893 
2894  // Build their equivalent archive rows
2895  $rowsInsert = [];
2896  $revids = [];
2897 
2899  $ipRevIds = [];
2900 
2901  $done = true;
2902  foreach ( $res as $row ) {
2903  if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2904  $done = false;
2905  break;
2906  }
2907 
2908  $comment = $commentStore->getComment( 'rev_comment', $row );
2909  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2910  $rowInsert = [
2911  'ar_namespace' => $namespace,
2912  'ar_title' => $dbKey,
2913  'ar_timestamp' => $row->rev_timestamp,
2914  'ar_minor_edit' => $row->rev_minor_edit,
2915  'ar_rev_id' => $row->rev_id,
2916  'ar_parent_id' => $row->rev_parent_id,
2925  'ar_len' => $row->rev_len,
2926  'ar_page_id' => $id,
2927  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2928  'ar_sha1' => $row->rev_sha1,
2929  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2930  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2931 
2932  if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
2933  $rowInsert['ar_text_id'] = $row->rev_text_id;
2934 
2935  if ( $wgContentHandlerUseDB ) {
2936  $rowInsert['ar_content_model'] = $row->rev_content_model;
2937  $rowInsert['ar_content_format'] = $row->rev_content_format;
2938  }
2939  }
2940 
2941  $rowsInsert[] = $rowInsert;
2942  $revids[] = $row->rev_id;
2943 
2944  // Keep track of IP edits, so that the corresponding rows can
2945  // be deleted in the ip_changes table.
2946  if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2947  $ipRevIds[] = $row->rev_id;
2948  }
2949  }
2950 
2951  // This conditional is just a sanity check
2952  if ( count( $revids ) > 0 ) {
2953  // Copy them into the archive table
2954  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2955 
2956  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2957  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2958  if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
2959  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2960  }
2961 
2962  // Also delete records from ip_changes as applicable.
2963  if ( count( $ipRevIds ) > 0 ) {
2964  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2965  }
2966  }
2967 
2968  return $done;
2969  }
2970 
2977  public function lockAndGetLatest() {
2978  return (int)wfGetDB( DB_MASTER )->selectField(
2979  'page',
2980  'page_latest',
2981  [
2982  'page_id' => $this->getId(),
2983  // Typically page_id is enough, but some code might try to do
2984  // updates assuming the title is the same, so verify that
2985  'page_namespace' => $this->getTitle()->getNamespace(),
2986  'page_title' => $this->getTitle()->getDBkey()
2987  ],
2988  __METHOD__,
2989  [ 'FOR UPDATE' ]
2990  );
2991  }
2992 
3005  public function doDeleteUpdates(
3006  $id, Content $content = null, Revision $revision = null, User $user = null
3007  ) {
3008  if ( $id !== $this->getId() ) {
3009  throw new InvalidArgumentException( 'Mismatching page ID' );
3010  }
3011 
3012  try {
3013  $countable = $this->isCountable();
3014  } catch ( Exception $ex ) {
3015  // fallback for deleting broken pages for which we cannot load the content for
3016  // some reason. Note that doDeleteArticleReal() already logged this problem.
3017  $countable = false;
3018  }
3019 
3020  // Update site status
3022  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3023  ) );
3024 
3025  // Delete pagelinks, update secondary indexes, etc
3026  $updates = $this->getDeletionUpdates(
3027  $revision ? $revision->getRevisionRecord() : $content
3028  );
3029  foreach ( $updates as $update ) {
3030  DeferredUpdates::addUpdate( $update );
3031  }
3032 
3033  $causeAgent = $user ? $user->getName() : 'unknown';
3034  // Reparse any pages transcluding this page
3036  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3037  // Reparse any pages including this image
3038  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3040  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3041  }
3042 
3043  // Clear caches
3044  self::onArticleDelete( $this->mTitle );
3046  $this->mTitle,
3047  $revision,
3048  null,
3050  );
3051 
3052  // Reset this object and the Title object
3053  $this->loadFromRow( false, self::READ_LATEST );
3054 
3055  // Search engine
3056  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3057  }
3058 
3088  public function doRollback(
3089  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3090  ) {
3091  $resultDetails = null;
3092 
3093  // Check permissions
3094  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3095  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3096  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3097 
3098  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3099  $errors[] = [ 'sessionfailure' ];
3100  }
3101 
3102  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3103  $errors[] = [ 'actionthrottledtext' ];
3104  }
3105 
3106  // If there were errors, bail out now
3107  if ( !empty( $errors ) ) {
3108  return $errors;
3109  }
3110 
3111  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3112  }
3113 
3134  public function commitRollback( $fromP, $summary, $bot,
3135  &$resultDetails, User $guser, $tags = null
3136  ) {
3137  global $wgUseRCPatrol;
3138 
3139  $dbw = wfGetDB( DB_MASTER );
3140 
3141  if ( wfReadOnly() ) {
3142  return [ [ 'readonlytext' ] ];
3143  }
3144 
3145  // Begin revision creation cycle by creating a PageUpdater.
3146  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3147  $updater = $this->newPageUpdater( $guser );
3148  $current = $updater->grabParentRevision();
3149 
3150  if ( is_null( $current ) ) {
3151  // Something wrong... no page?
3152  return [ [ 'notanarticle' ] ];
3153  }
3154 
3155  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3156  $legacyCurrent = new Revision( $current );
3157  $from = str_replace( '_', ' ', $fromP );
3158 
3159  // User name given should match up with the top revision.
3160  // If the revision's user is not visible, then $from should be empty.
3161  if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3162  $resultDetails = [ 'current' => $legacyCurrent ];
3163  return [ [ 'alreadyrolled',
3164  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3165  htmlspecialchars( $fromP ),
3166  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3167  ] ];
3168  }
3169 
3170  // Get the last edit not by this person...
3171  // Note: these may not be public values
3172  $actorWhere = ActorMigration::newMigration()->getWhere(
3173  $dbw,
3174  'rev_user',
3175  $current->getUser( RevisionRecord::RAW )
3176  );
3177 
3178  $s = $dbw->selectRow(
3179  [ 'revision' ] + $actorWhere['tables'],
3180  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3181  [
3182  'rev_page' => $current->getPageId(),
3183  'NOT(' . $actorWhere['conds'] . ')',
3184  ],
3185  __METHOD__,
3186  [
3187  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3188  'ORDER BY' => 'rev_timestamp DESC'
3189  ],
3190  $actorWhere['joins']
3191  );
3192  if ( $s === false ) {
3193  // No one else ever edited this page
3194  return [ [ 'cantrollback' ] ];
3195  } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3196  || $s->rev_deleted & RevisionRecord::DELETED_USER
3197  ) {
3198  // Only admins can see this text
3199  return [ [ 'notvisiblerev' ] ];
3200  }
3201 
3202  // Generate the edit summary if necessary
3203  $target = $this->getRevisionStore()->getRevisionById(
3204  $s->rev_id,
3205  RevisionStore::READ_LATEST
3206  );
3207  if ( empty( $summary ) ) {
3208  if ( !$currentEditorForPublic ) { // no public user name
3209  $summary = wfMessage( 'revertpage-nouser' );
3210  } else {
3211  $summary = wfMessage( 'revertpage' );
3212  }
3213  }
3214  $legacyTarget = new Revision( $target );
3215  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3216 
3217  // Allow the custom summary to use the same args as the default message
3218  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3219  $args = [
3220  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3221  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3222  $s->rev_id,
3223  $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3224  $current->getId(),
3225  $contLang->timeanddate( $current->getTimestamp() )
3226  ];
3227  if ( $summary instanceof Message ) {
3228  $summary = $summary->params( $args )->inContentLanguage()->text();
3229  } else {
3230  $summary = wfMsgReplaceArgs( $summary, $args );
3231  }
3232 
3233  // Trim spaces on user supplied text
3234  $summary = trim( $summary );
3235 
3236  // Save
3237  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3238 
3239  if ( $guser->isAllowed( 'minoredit' ) ) {
3240  $flags |= EDIT_MINOR;
3241  }
3242 
3243  if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3244  $flags |= EDIT_FORCE_BOT;
3245  }
3246 
3247  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3248  $currentContent = $current->getContent( SlotRecord::MAIN );
3249  $targetContent = $target->getContent( SlotRecord::MAIN );
3250  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3251 
3252  if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3253  $tags[] = 'mw-rollback';
3254  }
3255 
3256  // Build rollback revision:
3257  // Restore old content
3258  // TODO: MCR: test this once we can store multiple slots
3259  foreach ( $target->getSlots()->getSlots() as $slot ) {
3260  $updater->inheritSlot( $slot );
3261  }
3262 
3263  // Remove extra slots
3264  // TODO: MCR: test this once we can store multiple slots
3265  foreach ( $current->getSlotRoles() as $role ) {
3266  if ( !$target->hasSlot( $role ) ) {
3267  $updater->removeSlot( $role );
3268  }
3269  }
3270 
3271  $updater->setOriginalRevisionId( $target->getId() );
3272  // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3273  $updater->addTags( $tags );
3274 
3275  // TODO: this logic should not be in the storage layer, it's here for compatibility
3276  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3277  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3278  if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $guser ) ) {
3279  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3280  }
3281 
3282  // Actually store the rollback
3283  $rev = $updater->saveRevision(
3285  $flags
3286  );
3287 
3288  // Set patrolling and bot flag on the edits, which gets rollbacked.
3289  // This is done even on edit failure to have patrolling in that case (T64157).
3290  $set = [];
3291  if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3292  // Mark all reverted edits as bot
3293  $set['rc_bot'] = 1;
3294  }
3295 
3296  if ( $wgUseRCPatrol ) {
3297  // Mark all reverted edits as patrolled
3298  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3299  }
3300 
3301  if ( count( $set ) ) {
3302  $actorWhere = ActorMigration::newMigration()->getWhere(
3303  $dbw,
3304  'rc_user',
3305  $current->getUser( RevisionRecord::RAW ),
3306  false
3307  );
3308  $dbw->update( 'recentchanges', $set,
3309  [ /* WHERE */
3310  'rc_cur_id' => $current->getPageId(),
3311  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3312  $actorWhere['conds'], // No tables/joins are needed for rc_user
3313  ],
3314  __METHOD__
3315  );
3316  }
3317 
3318  if ( !$updater->wasSuccessful() ) {
3319  return $updater->getStatus()->getErrorsArray();
3320  }
3321 
3322  // Report if the edit was not created because it did not change the content.
3323  if ( $updater->isUnchanged() ) {
3324  $resultDetails = [ 'current' => $legacyCurrent ];
3325  return [ [ 'alreadyrolled',
3326  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3327  htmlspecialchars( $fromP ),
3328  htmlspecialchars( $targetEditorForPublic ? $targetEditorForPublic->getName() : '' )
3329  ] ];
3330  }
3331 
3332  if ( $changingContentModel ) {
3333  // If the content model changed during the rollback,
3334  // make sure it gets logged to Special:Log/contentmodel
3335  $log = new ManualLogEntry( 'contentmodel', 'change' );
3336  $log->setPerformer( $guser );
3337  $log->setTarget( $this->mTitle );
3338  $log->setComment( $summary );
3339  $log->setParameters( [
3340  '4::oldmodel' => $currentContent->getModel(),
3341  '5::newmodel' => $targetContent->getModel(),
3342  ] );
3343 
3344  $logId = $log->insert( $dbw );
3345  $log->publish( $logId );
3346  }
3347 
3348  $revId = $rev->getId();
3349 
3350  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3351 
3352  $resultDetails = [
3353  'summary' => $summary,
3354  'current' => $legacyCurrent,
3355  'target' => $legacyTarget,
3356  'newid' => $revId,
3357  'tags' => $tags
3358  ];
3359 
3360  // TODO: make this return a Status object and wrap $resultDetails in that.
3361  return [];
3362  }
3363 
3375  public static function onArticleCreate( Title $title ) {
3376  // TODO: move this into a PageEventEmitter service
3377 
3378  // Update existence markers on article/talk tabs...
3379  $other = $title->getOtherPage();
3380 
3381  $other->purgeSquid();
3382 
3383  $title->touchLinks();
3384  $title->purgeSquid();
3385  $title->deleteTitleProtection();
3386 
3387  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3388 
3389  // Invalidate caches of articles which include this page
3391  new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
3392  );
3393 
3394  if ( $title->getNamespace() == NS_CATEGORY ) {
3395  // Load the Category object, which will schedule a job to create
3396  // the category table row if necessary. Checking a replica DB is ok
3397  // here, in the worst case it'll run an unnecessary recount job on
3398  // a category that probably doesn't have many members.
3399  Category::newFromTitle( $title )->getID();
3400  }
3401  }
3402 
3408  public static function onArticleDelete( Title $title ) {
3409  // TODO: move this into a PageEventEmitter service
3410 
3411  // Update existence markers on article/talk tabs...
3412  // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3413  BacklinkCache::get( $title )->clear();
3414  $other = $title->getOtherPage();
3415 
3416  $other->purgeSquid();
3417 
3418  $title->touchLinks();
3419  $title->purgeSquid();
3420 
3421  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3422 
3423  // File cache
3425  InfoAction::invalidateCache( $title );
3426 
3427  // Messages
3428  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3429  MessageCache::singleton()->updateMessageOverride( $title, null );
3430  }
3431 
3432  // Images
3433  if ( $title->getNamespace() == NS_FILE ) {
3435  new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
3436  );
3437  }
3438 
3439  // User talk pages
3440  if ( $title->getNamespace() == NS_USER_TALK ) {
3441  $user = User::newFromName( $title->getText(), false );
3442  if ( $user ) {
3443  $user->setNewtalk( false );
3444  }
3445  }
3446 
3447  // Image redirects
3448  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3449 
3450  // Purge cross-wiki cache entities referencing this page
3451  self::purgeInterwikiCheckKey( $title );
3452  }
3453 
3462  public static function onArticleEdit(
3463  Title $title,
3464  Revision $revision = null,
3465  $slotsChanged = null
3466  ) {
3467  // TODO: move this into a PageEventEmitter service
3468 
3469  if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3470  // Invalidate caches of articles which include this page.
3471  // Only for the main slot, because only the main slot is transcluded.
3472  // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3474  new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
3475  );
3476  }
3477 
3478  // Invalidate the caches of all pages which redirect here
3480  new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
3481  );
3482 
3483  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3484 
3485  // Purge CDN for this page only
3486  $title->purgeSquid();
3487  // Clear file cache for this page only
3489 
3490  // Purge ?action=info cache
3491  $revid = $revision ? $revision->getId() : null;
3492  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3493  InfoAction::invalidateCache( $title, $revid );
3494  } );
3495 
3496  // Purge cross-wiki cache entities referencing this page
3497  self::purgeInterwikiCheckKey( $title );
3498  }
3499 
3507  private static function purgeInterwikiCheckKey( Title $title ) {
3509 
3510  if ( !$wgEnableScaryTranscluding ) {
3511  return; // @todo: perhaps this wiki is only used as a *source* for content?
3512  }
3513 
3514  DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3515  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3516  $cache->resetCheckKey(
3517  // Do not include the namespace since there can be multiple aliases to it
3518  // due to different namespace text definitions on different wikis. This only
3519  // means that some cache invalidations happen that are not strictly needed.
3520  $cache->makeGlobalKey(
3521  'interwiki-page',
3523  $title->getDBkey()
3524  )
3525  );
3526  } );
3527  }
3528 
3535  public function getCategories() {
3536  $id = $this->getId();
3537  if ( $id == 0 ) {
3538  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3539  }
3540 
3541  $dbr = wfGetDB( DB_REPLICA );
3542  $res = $dbr->select( 'categorylinks',
3543  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3544  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3545  // as not being aliases, and NS_CATEGORY is numeric
3546  [ 'cl_from' => $id ],
3547  __METHOD__ );
3548 
3549  return TitleArray::newFromResult( $res );
3550  }
3551 
3558  public function getHiddenCategories() {
3559  $result = [];
3560  $id = $this->getId();
3561 
3562  if ( $id == 0 ) {
3563  return [];
3564  }
3565 
3566  $dbr = wfGetDB( DB_REPLICA );
3567  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3568  [ 'cl_to' ],
3569  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3570  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3571  __METHOD__ );
3572 
3573  if ( $res !== false ) {
3574  foreach ( $res as $row ) {
3575  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3576  }
3577  }
3578 
3579  return $result;
3580  }
3581 
3589  public function getAutoDeleteReason( &$hasHistory ) {
3590  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3591  }
3592 
3603  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3604  $id = $id ?: $this->getId();
3605  $type = MWNamespace::getCategoryLinkType( $this->getTitle()->getNamespace() );
3606 
3607  $addFields = [ 'cat_pages = cat_pages + 1' ];
3608  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3609  if ( $type !== 'page' ) {
3610  $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3611  $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3612  }
3613 
3614  $dbw = wfGetDB( DB_MASTER );
3615 
3616  if ( count( $added ) ) {
3617  $existingAdded = $dbw->selectFieldValues(
3618  'category',
3619  'cat_title',
3620  [ 'cat_title' => $added ],
3621  __METHOD__
3622  );
3623 
3624  // For category rows that already exist, do a plain
3625  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3626  // to avoid creating gaps in the cat_id sequence.
3627  if ( count( $existingAdded ) ) {
3628  $dbw->update(
3629  'category',
3630  $addFields,
3631  [ 'cat_title' => $existingAdded ],
3632  __METHOD__
3633  );
3634  }
3635 
3636  $missingAdded = array_diff( $added, $existingAdded );
3637  if ( count( $missingAdded ) ) {
3638  $insertRows = [];
3639  foreach ( $missingAdded as $cat ) {
3640  $insertRows[] = [
3641  'cat_title' => $cat,
3642  'cat_pages' => 1,
3643  'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3644  'cat_files' => ( $type === 'file' ) ? 1 : 0,
3645  ];
3646  }
3647  $dbw->upsert(
3648  'category',
3649  $insertRows,
3650  [ 'cat_title' ],
3651  $addFields,
3652  __METHOD__
3653  );
3654  }
3655  }
3656 
3657  if ( count( $deleted ) ) {
3658  $dbw->update(
3659  'category',
3660  $removeFields,
3661  [ 'cat_title' => $deleted ],
3662  __METHOD__
3663  );
3664  }
3665 
3666  foreach ( $added as $catName ) {
3667  $cat = Category::newFromName( $catName );
3668  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3669  }
3670 
3671  foreach ( $deleted as $catName ) {
3672  $cat = Category::newFromName( $catName );
3673  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3674  // Refresh counts on categories that should be empty now (after commit, T166757)
3675  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3676  $cat->refreshCountsIfEmpty();
3677  } );
3678  }
3679  }
3680 
3687  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3688  if ( wfReadOnly() ) {
3689  return;
3690  }
3691 
3692  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3693  [ $this, $this->mTitle, $parserOutput ]
3694  ) ) {
3695  return;
3696  }
3697 
3698  $config = RequestContext::getMain()->getConfig();
3699 
3700  $params = [
3701  'isOpportunistic' => true,
3702  'rootJobTimestamp' => $parserOutput->getCacheTime()
3703  ];
3704 
3705  if ( $this->mTitle->areRestrictionsCascading() ) {
3706  // If the page is cascade protecting, the links should really be up-to-date
3707  JobQueueGroup::singleton()->lazyPush(
3708  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3709  );
3710  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3711  // Assume the output contains "dynamic" time/random based magic words.
3712  // Only update pages that expired due to dynamic content and NOT due to edits
3713  // to referenced templates/files. When the cache expires due to dynamic content,
3714  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3715  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3716  // template/file edit already triggered recursive RefreshLinksJob jobs.
3717  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3718  // If a page is uncacheable, do not keep spamming a job for it.
3719  // Although it would be de-duplicated, it would still waste I/O.
3721  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3722  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3723  if ( $cache->add( $key, time(), $ttl ) ) {
3724  JobQueueGroup::singleton()->lazyPush(
3725  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3726  );
3727  }
3728  }
3729  }
3730  }
3731 
3741  public function getDeletionUpdates( $rev = null ) {
3742  if ( !$rev ) {
3743  wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3744 
3745  try {
3746  $rev = $this->getRevisionRecord();
3747  } catch ( Exception $ex ) {
3748  // If we can't load the content, something is wrong. Perhaps that's why
3749  // the user is trying to delete the page, so let's not fail in that case.
3750  // Note that doDeleteArticleReal() will already have logged an issue with
3751  // loading the content.
3752  wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3753  }
3754  }
3755 
3756  if ( !$rev ) {
3757  $slotContent = [];
3758  } elseif ( $rev instanceof Content ) {
3759  wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3760 
3761  $slotContent = [ SlotRecord::MAIN => $rev ];
3762  } else {
3763  $slotContent = array_map( function ( SlotRecord $slot ) {
3764  return $slot->getContent( Revision::RAW );
3765  }, $rev->getSlots()->getSlots() );
3766  }
3767 
3768  $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3769 
3770  // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3771  // model here, not the content object!
3772  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3774  foreach ( $slotContent as $role => $content ) {
3775  $handler = $content->getContentHandler();
3776 
3777  $updates = $handler->getDeletionUpdates(
3778  $this->getTitle(),
3779  $role
3780  );
3781  $allUpdates = array_merge( $allUpdates, $updates );
3782 
3783  // TODO: remove B/C hack in 1.32!
3784  $legacyUpdates = $content->getDeletionUpdates( $this );
3785 
3786  // HACK: filter out redundant and incomplete LinksDeletionUpdate
3787  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3788  return !( $update instanceof LinksDeletionUpdate );
3789  } );
3790 
3791  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3792  }
3793 
3794  Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3795 
3796  // TODO: hard deprecate old hook in 1.33
3797  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3798  return $allUpdates;
3799  }
3800 
3808  public function isLocal() {
3809  return true;
3810  }
3811 
3821  public function getWikiDisplayName() {
3822  global $wgSitename;
3823  return $wgSitename;
3824  }
3825 
3834  public function getSourceURL() {
3835  return $this->getTitle()->getCanonicalURL();
3836  }
3837 
3844  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3845 
3846  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3847  }
3848 
3849 }
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:3399
updateParserCache(array $options=[])
Update the parser cache.
Definition: WikiPage.php:2080
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:3375
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4838
$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:1514
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:3821
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:917
clearNotification(&$title, $oldid=0)
Clear the user&#39;s notification timestamp for the given title.
Definition: User.php:3979
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:1985
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:3603
getTimestamp()
Definition: Revision.php:994
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3837
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2472
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:1042
Title $mTitle
Definition: WikiPage.php:51
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage.
static purgeInterwikiCheckKey(Title $title)
#-
Definition: WikiPage.php:3507
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1266
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:3687
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3834
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:1484
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
Definition: WikiPage.php:2664
static getLocalClusterInstance()
Get the main cluster-local cache object.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
Value object representing a modification of revision slots.
getContributors()
Get a list of users who have edited this article, not including the user who made the most recent rev...
Definition: WikiPage.php:1151
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
newDerivedDataUpdater()
Definition: WikiPage.php:1685
const EDIT_MINOR
Definition: Defines.php: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:476
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:3843
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:2460
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object&#39;s namespace and title using a sanitized title string...
Definition: WikiPage.php:455
getActionOverrides()
Definition: WikiPage.php:273
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:630
Class DeletePageJob.
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:302
getRevisionStore()
Definition: WikiPage.php:235
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. '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:1983
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:3462
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:47
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:3134
doDeleteArticleReal( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $deleter=null, $tags=[], $logsubtype='delete', $immediate=false)
Back-end article deletion Deletes the article with database consistency, writes logs, purges caches.
Definition: WikiPage.php:2626
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
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:2952
static getMain()
Get the RequestContext object associated with the main request.
isBatchedDelete( $safetyMargin=0)
Determines if deletion of this page would be batched (executed over time by the job queue) or not (co...
Definition: WikiPage.php:2564
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:2593
doDeleteUpdates( $id, Content $content=null, Revision $revision=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:3005
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:935
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:1327
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()
Get a RepoGroup instance.
Definition: RepoGroup.php:61
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:54
__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:3558
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:3867
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:1985
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:959
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:1769
int false $mLatest
False means "not loaded".
Definition: WikiPage.php:69
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:845
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:651
Special handling for file pages.
const NS_MEDIAWIKI
Definition: Defines.php:72
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:286
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1287
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
Definition: WikiPage.php:2121
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3589
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2407
getUndoContent(Revision $undo, Revision $undoafter)
Get the content that needs to be saved in order to undo all revisions between $undo and $undoafter...
Definition: WikiPage.php:1545
bool $mDataLoaded
Definition: WikiPage.php:57
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
$wgPageCreationLog
Maintain a log of page creations at Special:Log/create?
getStubThreshold()
Thumb size preferred by the user.
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php: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:546
static getCurrentWikiDbDomain()
Definition: WikiMap.php:286
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:608
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:675
wfRandom()
Get a random decimal value in the domain of [0, 1), in a way not likely to give duplicate values for ...
getUserText( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php: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:3535
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:2052
getId()
Get the user&#39;s ID.
Definition: User.php:2433
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4681
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:2132
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3808
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:3741
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3408
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:3088
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:2621
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:995
formatExpiry( $expiry)
Definition: WikiPage.php:2450
bool $mIsRedirect
Definition: WikiPage.php:63
MediaWiki Logger LoggerFactory implements a PSR [0] compatible message logging system Named Psr Log LoggerInterface instances can be obtained from the MediaWiki Logger LoggerFactory::getInstance() static method. MediaWiki\Logger\LoggerFactory expects a class implementing the MediaWiki\Logger\Spi interface to act as a factory for new Psr\Log\LoggerInterface instances. The "Spi" in MediaWiki\Logger\Spi stands for "service provider interface". An SPI is an API intended to be implemented or extended by a third party. This software design pattern is intended to enable framework extension and replaceable components. It is specifically used in the MediaWiki\Logger\LoggerFactory service to allow alternate PSR-3 logging implementations to be easily integrated with MediaWiki. The service provider interface allows the backend logging library to be implemented in multiple ways. The $wgMWLoggerDefaultSpi global provides the classname of the default MediaWiki\Logger\Spi implementation to be loaded at runtime. This can either be the name of a class implementing the MediaWiki\Logger\Spi with a zero argument const ructor or a callable that will return an MediaWiki\Logger\Spi instance. Alternately the MediaWiki\Logger\LoggerFactory MediaWiki Logger LoggerFactory
Definition: logger.txt:5
static singleton( $domain=false)
insertRedirectEntry(Title $rt, $oldLatest=null)
Insert or update the redirect table entry for this page to indicate it redirects to $rt...
Definition: WikiPage.php:1067
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1223
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1374
Page revision base class.
isSafeToCache()
Test whether these options are safe to cache.
static flattenRestrictions( $limit)
Take an array of page restrictions and flatten it to a string suitable for insertion into the page_re...
Definition: WikiPage.php:2537
const DB_REPLICA
Definition: defines.php:25
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2977
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:584
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:1977
$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:1198
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:2829
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:3955
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1476
static getCategoryLinkType( $index)
Returns the link type to be used for categories.
doUpdateRestrictions(array $limit, array $expiry, &$cascade, $reason, User $user, $tags=null)
Update the article&#39;s restriction field, and leave a log entry.
Definition: WikiPage.php:2151
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:2514