MediaWiki  1.31.0
WikiPage.php
Go to the documentation of this file.
1 <?php
26 use Wikimedia\Assert\Assert;
30 
37 class WikiPage implements Page, IDBAccessObject {
38  // Constants for $mDataLoadedFrom and related
39 
43  public $mTitle = null;
44 
48  public $mDataLoaded = false; // !< Boolean
49  public $mIsRedirect = false; // !< Boolean
50  public $mLatest = false; // !< Integer (false means "not loaded")
54  public $mPreparedEdit = false;
55 
59  protected $mId = null;
60 
65 
69  protected $mRedirectTarget = null;
70 
74  protected $mLastRevision = null;
75 
79  protected $mTimestamp = '';
80 
84  protected $mTouched = '19700101000000';
85 
89  protected $mLinksUpdated = '19700101000000';
90 
95  public function __construct( Title $title ) {
96  $this->mTitle = $title;
97  }
98 
103  public function __clone() {
104  $this->mTitle = clone $this->mTitle;
105  }
106 
115  public static function factory( Title $title ) {
116  $ns = $title->getNamespace();
117 
118  if ( $ns == NS_MEDIA ) {
119  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
120  } elseif ( $ns < 0 ) {
121  throw new MWException( "Invalid or virtual namespace $ns given." );
122  }
123 
124  $page = null;
125  if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
126  return $page;
127  }
128 
129  switch ( $ns ) {
130  case NS_FILE:
131  $page = new WikiFilePage( $title );
132  break;
133  case NS_CATEGORY:
134  $page = new WikiCategoryPage( $title );
135  break;
136  default:
137  $page = new WikiPage( $title );
138  }
139 
140  return $page;
141  }
142 
153  public static function newFromID( $id, $from = 'fromdb' ) {
154  // page ids are never 0 or negative, see T63166
155  if ( $id < 1 ) {
156  return null;
157  }
158 
159  $from = self::convertSelectType( $from );
160  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
161  $pageQuery = self::getQueryInfo();
162  $row = $db->selectRow(
163  $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
164  [], $pageQuery['joins']
165  );
166  if ( !$row ) {
167  return null;
168  }
169  return self::newFromRow( $row, $from );
170  }
171 
183  public static function newFromRow( $row, $from = 'fromdb' ) {
184  $page = self::factory( Title::newFromRow( $row ) );
185  $page->loadFromRow( $row, $from );
186  return $page;
187  }
188 
195  private static function convertSelectType( $type ) {
196  switch ( $type ) {
197  case 'fromdb':
198  return self::READ_NORMAL;
199  case 'fromdbmaster':
200  return self::READ_LATEST;
201  case 'forupdate':
202  return self::READ_LOCKING;
203  default:
204  // It may already be an integer or whatever else
205  return $type;
206  }
207  }
208 
215  public function getActionOverrides() {
216  return $this->getContentHandler()->getActionOverrides();
217  }
218 
228  public function getContentHandler() {
230  }
231 
236  public function getTitle() {
237  return $this->mTitle;
238  }
239 
244  public function clear() {
245  $this->mDataLoaded = false;
246  $this->mDataLoadedFrom = self::READ_NONE;
247 
248  $this->clearCacheFields();
249  }
250 
255  protected function clearCacheFields() {
256  $this->mId = null;
257  $this->mRedirectTarget = null; // Title object if set
258  $this->mLastRevision = null; // Latest revision
259  $this->mTouched = '19700101000000';
260  $this->mLinksUpdated = '19700101000000';
261  $this->mTimestamp = '';
262  $this->mIsRedirect = false;
263  $this->mLatest = false;
264  // T59026: do not clear mPreparedEdit since prepareTextForEdit() already checks
265  // the requested rev ID and content against the cached one for equality. For most
266  // content types, the output should not change during the lifetime of this cache.
267  // Clearing it can cause extra parses on edit for no reason.
268  }
269 
275  public function clearPreparedEdit() {
276  $this->mPreparedEdit = false;
277  }
278 
286  public static function selectFields() {
288 
289  wfDeprecated( __METHOD__, '1.31' );
290 
291  $fields = [
292  'page_id',
293  'page_namespace',
294  'page_title',
295  'page_restrictions',
296  'page_is_redirect',
297  'page_is_new',
298  'page_random',
299  'page_touched',
300  'page_links_updated',
301  'page_latest',
302  'page_len',
303  ];
304 
305  if ( $wgContentHandlerUseDB ) {
306  $fields[] = 'page_content_model';
307  }
308 
309  if ( $wgPageLanguageUseDB ) {
310  $fields[] = 'page_lang';
311  }
312 
313  return $fields;
314  }
315 
325  public static function getQueryInfo() {
327 
328  $ret = [
329  'tables' => [ 'page' ],
330  'fields' => [
331  'page_id',
332  'page_namespace',
333  'page_title',
334  'page_restrictions',
335  'page_is_redirect',
336  'page_is_new',
337  'page_random',
338  'page_touched',
339  'page_links_updated',
340  'page_latest',
341  'page_len',
342  ],
343  'joins' => [],
344  ];
345 
346  if ( $wgContentHandlerUseDB ) {
347  $ret['fields'][] = 'page_content_model';
348  }
349 
350  if ( $wgPageLanguageUseDB ) {
351  $ret['fields'][] = 'page_lang';
352  }
353 
354  return $ret;
355  }
356 
364  protected function pageData( $dbr, $conditions, $options = [] ) {
365  $pageQuery = self::getQueryInfo();
366 
367  // Avoid PHP 7.1 warning of passing $this by reference
368  $wikiPage = $this;
369 
370  Hooks::run( 'ArticlePageDataBefore', [
371  &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
372  ] );
373 
374  $row = $dbr->selectRow(
375  $pageQuery['tables'],
376  $pageQuery['fields'],
377  $conditions,
378  __METHOD__,
379  $options,
380  $pageQuery['joins']
381  );
382 
383  Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
384 
385  return $row;
386  }
387 
397  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
398  return $this->pageData( $dbr, [
399  'page_namespace' => $title->getNamespace(),
400  'page_title' => $title->getDBkey() ], $options );
401  }
402 
411  public function pageDataFromId( $dbr, $id, $options = [] ) {
412  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
413  }
414 
427  public function loadPageData( $from = 'fromdb' ) {
428  $from = self::convertSelectType( $from );
429  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
430  // We already have the data from the correct location, no need to load it twice.
431  return;
432  }
433 
434  if ( is_int( $from ) ) {
435  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
436  $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
437  $db = $loadBalancer->getConnection( $index );
438  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
439 
440  if ( !$data
441  && $index == DB_REPLICA
442  && $loadBalancer->getServerCount() > 1
443  && $loadBalancer->hasOrMadeRecentMasterChanges()
444  ) {
445  $from = self::READ_LATEST;
446  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
447  $db = $loadBalancer->getConnection( $index );
448  $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
449  }
450  } else {
451  // No idea from where the caller got this data, assume replica DB.
452  $data = $from;
453  $from = self::READ_NORMAL;
454  }
455 
456  $this->loadFromRow( $data, $from );
457  }
458 
470  public function loadFromRow( $data, $from ) {
471  $lc = LinkCache::singleton();
472  $lc->clearLink( $this->mTitle );
473 
474  if ( $data ) {
475  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
476 
477  $this->mTitle->loadFromRow( $data );
478 
479  // Old-fashioned restrictions
480  $this->mTitle->loadRestrictions( $data->page_restrictions );
481 
482  $this->mId = intval( $data->page_id );
483  $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
484  $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
485  $this->mIsRedirect = intval( $data->page_is_redirect );
486  $this->mLatest = intval( $data->page_latest );
487  // T39225: $latest may no longer match the cached latest Revision object.
488  // Double-check the ID of any cached latest Revision object for consistency.
489  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
490  $this->mLastRevision = null;
491  $this->mTimestamp = '';
492  }
493  } else {
494  $lc->addBadLinkObj( $this->mTitle );
495 
496  $this->mTitle->loadFromRow( false );
497 
498  $this->clearCacheFields();
499 
500  $this->mId = 0;
501  }
502 
503  $this->mDataLoaded = true;
504  $this->mDataLoadedFrom = self::convertSelectType( $from );
505  }
506 
510  public function getId() {
511  if ( !$this->mDataLoaded ) {
512  $this->loadPageData();
513  }
514  return $this->mId;
515  }
516 
520  public function exists() {
521  if ( !$this->mDataLoaded ) {
522  $this->loadPageData();
523  }
524  return $this->mId > 0;
525  }
526 
535  public function hasViewableContent() {
536  return $this->mTitle->isKnown();
537  }
538 
544  public function isRedirect() {
545  if ( !$this->mDataLoaded ) {
546  $this->loadPageData();
547  }
548 
549  return (bool)$this->mIsRedirect;
550  }
551 
562  public function getContentModel() {
563  if ( $this->exists() ) {
565 
566  return $cache->getWithSetCallback(
567  $cache->makeKey( 'page-content-model', $this->getLatest() ),
568  $cache::TTL_MONTH,
569  function () {
570  $rev = $this->getRevision();
571  if ( $rev ) {
572  // Look at the revision's actual content model
573  return $rev->getContentModel();
574  } else {
575  $title = $this->mTitle->getPrefixedDBkey();
576  wfWarn( "Page $title exists but has no (visible) revisions!" );
577  return $this->mTitle->getContentModel();
578  }
579  }
580  );
581  }
582 
583  // use the default model for this page
584  return $this->mTitle->getContentModel();
585  }
586 
591  public function checkTouched() {
592  if ( !$this->mDataLoaded ) {
593  $this->loadPageData();
594  }
595  return ( $this->mId && !$this->mIsRedirect );
596  }
597 
602  public function getTouched() {
603  if ( !$this->mDataLoaded ) {
604  $this->loadPageData();
605  }
606  return $this->mTouched;
607  }
608 
613  public function getLinksTimestamp() {
614  if ( !$this->mDataLoaded ) {
615  $this->loadPageData();
616  }
617  return $this->mLinksUpdated;
618  }
619 
624  public function getLatest() {
625  if ( !$this->mDataLoaded ) {
626  $this->loadPageData();
627  }
628  return (int)$this->mLatest;
629  }
630 
635  public function getOldestRevision() {
636  // Try using the replica DB first, then try the master
637  $rev = $this->mTitle->getFirstRevision();
638  if ( !$rev ) {
639  $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE );
640  }
641  return $rev;
642  }
643 
648  protected function loadLastEdit() {
649  if ( $this->mLastRevision !== null ) {
650  return; // already loaded
651  }
652 
653  $latest = $this->getLatest();
654  if ( !$latest ) {
655  return; // page doesn't exist or is missing page_latest info
656  }
657 
658  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
659  // T39225: if session S1 loads the page row FOR UPDATE, the result always
660  // includes the latest changes committed. This is true even within REPEATABLE-READ
661  // transactions, where S1 normally only sees changes committed before the first S1
662  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
663  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
664  // happened after the first S1 SELECT.
665  // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
666  $flags = Revision::READ_LOCKING;
667  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
668  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
669  // Bug T93976: if page_latest was loaded from the master, fetch the
670  // revision from there as well, as it may not exist yet on a replica DB.
671  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
672  $flags = Revision::READ_LATEST;
673  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
674  } else {
675  $dbr = wfGetDB( DB_REPLICA );
676  $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
677  }
678 
679  if ( $revision ) { // sanity
680  $this->setLastEdit( $revision );
681  }
682  }
683 
688  protected function setLastEdit( Revision $revision ) {
689  $this->mLastRevision = $revision;
690  $this->mTimestamp = $revision->getTimestamp();
691  }
692 
697  public function getRevision() {
698  $this->loadLastEdit();
699  if ( $this->mLastRevision ) {
700  return $this->mLastRevision;
701  }
702  return null;
703  }
704 
718  public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
719  $this->loadLastEdit();
720  if ( $this->mLastRevision ) {
721  return $this->mLastRevision->getContent( $audience, $user );
722  }
723  return null;
724  }
725 
729  public function getTimestamp() {
730  // Check if the field has been filled by WikiPage::setTimestamp()
731  if ( !$this->mTimestamp ) {
732  $this->loadLastEdit();
733  }
734 
735  return wfTimestamp( TS_MW, $this->mTimestamp );
736  }
737 
743  public function setTimestamp( $ts ) {
744  $this->mTimestamp = wfTimestamp( TS_MW, $ts );
745  }
746 
756  public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
757  $this->loadLastEdit();
758  if ( $this->mLastRevision ) {
759  return $this->mLastRevision->getUser( $audience, $user );
760  } else {
761  return -1;
762  }
763  }
764 
775  public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
776  $revision = $this->getOldestRevision();
777  if ( $revision ) {
778  $userName = $revision->getUserText( $audience, $user );
779  return User::newFromName( $userName, false );
780  } else {
781  return null;
782  }
783  }
784 
794  public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
795  $this->loadLastEdit();
796  if ( $this->mLastRevision ) {
797  return $this->mLastRevision->getUserText( $audience, $user );
798  } else {
799  return '';
800  }
801  }
802 
812  public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
813  $this->loadLastEdit();
814  if ( $this->mLastRevision ) {
815  return $this->mLastRevision->getComment( $audience, $user );
816  } else {
817  return '';
818  }
819  }
820 
826  public function getMinorEdit() {
827  $this->loadLastEdit();
828  if ( $this->mLastRevision ) {
829  return $this->mLastRevision->isMinor();
830  } else {
831  return false;
832  }
833  }
834 
843  public function isCountable( $editInfo = false ) {
845 
846  if ( !$this->mTitle->isContentPage() ) {
847  return false;
848  }
849 
850  if ( $editInfo ) {
851  $content = $editInfo->pstContent;
852  } else {
853  $content = $this->getContent();
854  }
855 
856  if ( !$content || $content->isRedirect() ) {
857  return false;
858  }
859 
860  $hasLinks = null;
861 
862  if ( $wgArticleCountMethod === 'link' ) {
863  // nasty special case to avoid re-parsing to detect links
864 
865  if ( $editInfo ) {
866  // ParserOutput::getLinks() is a 2D array of page links, so
867  // to be really correct we would need to recurse in the array
868  // but the main array should only have items in it if there are
869  // links.
870  $hasLinks = (bool)count( $editInfo->output->getLinks() );
871  } else {
872  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
873  [ 'pl_from' => $this->getId() ], __METHOD__ );
874  }
875  }
876 
877  return $content->isCountable( $hasLinks );
878  }
879 
887  public function getRedirectTarget() {
888  if ( !$this->mTitle->isRedirect() ) {
889  return null;
890  }
891 
892  if ( $this->mRedirectTarget !== null ) {
893  return $this->mRedirectTarget;
894  }
895 
896  // Query the redirect table
897  $dbr = wfGetDB( DB_REPLICA );
898  $row = $dbr->selectRow( 'redirect',
899  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
900  [ 'rd_from' => $this->getId() ],
901  __METHOD__
902  );
903 
904  // rd_fragment and rd_interwiki were added later, populate them if empty
905  if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
906  $this->mRedirectTarget = Title::makeTitle(
907  $row->rd_namespace, $row->rd_title,
908  $row->rd_fragment, $row->rd_interwiki
909  );
910  return $this->mRedirectTarget;
911  }
912 
913  // This page doesn't have an entry in the redirect table
914  $this->mRedirectTarget = $this->insertRedirect();
915  return $this->mRedirectTarget;
916  }
917 
926  public function insertRedirect() {
927  $content = $this->getContent();
928  $retval = $content ? $content->getUltimateRedirectTarget() : null;
929  if ( !$retval ) {
930  return null;
931  }
932 
933  // Update the DB post-send if the page has not cached since now
934  $latest = $this->getLatest();
936  function () use ( $retval, $latest ) {
937  $this->insertRedirectEntry( $retval, $latest );
938  },
940  wfGetDB( DB_MASTER )
941  );
942 
943  return $retval;
944  }
945 
951  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
952  $dbw = wfGetDB( DB_MASTER );
953  $dbw->startAtomic( __METHOD__ );
954 
955  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
956  $dbw->upsert(
957  'redirect',
958  [
959  'rd_from' => $this->getId(),
960  'rd_namespace' => $rt->getNamespace(),
961  'rd_title' => $rt->getDBkey(),
962  'rd_fragment' => $rt->getFragment(),
963  'rd_interwiki' => $rt->getInterwiki(),
964  ],
965  [ 'rd_from' ],
966  [
967  'rd_namespace' => $rt->getNamespace(),
968  'rd_title' => $rt->getDBkey(),
969  'rd_fragment' => $rt->getFragment(),
970  'rd_interwiki' => $rt->getInterwiki(),
971  ],
972  __METHOD__
973  );
974  }
975 
976  $dbw->endAtomic( __METHOD__ );
977  }
978 
984  public function followRedirect() {
985  return $this->getRedirectURL( $this->getRedirectTarget() );
986  }
987 
995  public function getRedirectURL( $rt ) {
996  if ( !$rt ) {
997  return false;
998  }
999 
1000  if ( $rt->isExternal() ) {
1001  if ( $rt->isLocal() ) {
1002  // Offsite wikis need an HTTP redirect.
1003  // This can be hard to reverse and may produce loops,
1004  // so they may be disabled in the site configuration.
1005  $source = $this->mTitle->getFullURL( 'redirect=no' );
1006  return $rt->getFullURL( [ 'rdfrom' => $source ] );
1007  } else {
1008  // External pages without "local" bit set are not valid
1009  // redirect targets
1010  return false;
1011  }
1012  }
1013 
1014  if ( $rt->isSpecialPage() ) {
1015  // Gotta handle redirects to special pages differently:
1016  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1017  // Some pages are not valid targets.
1018  if ( $rt->isValidRedirectTarget() ) {
1019  return $rt->getFullURL();
1020  } else {
1021  return false;
1022  }
1023  }
1024 
1025  return $rt;
1026  }
1027 
1033  public function getContributors() {
1034  // @todo FIXME: This is expensive; cache this info somewhere.
1035 
1036  $dbr = wfGetDB( DB_REPLICA );
1037 
1038  $actorMigration = ActorMigration::newMigration();
1039  $actorQuery = $actorMigration->getJoin( 'rev_user' );
1040 
1041  $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1042 
1043  $fields = [
1044  'user_id' => $actorQuery['fields']['rev_user'],
1045  'user_name' => $actorQuery['fields']['rev_user_text'],
1046  'actor_id' => $actorQuery['fields']['rev_actor'],
1047  'user_real_name' => 'MIN(user_real_name)',
1048  'timestamp' => 'MAX(rev_timestamp)',
1049  ];
1050 
1051  $conds = [ 'rev_page' => $this->getId() ];
1052 
1053  // The user who made the top revision gets credited as "this page was last edited by
1054  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1055  $user = $this->getUser()
1056  ? User::newFromId( $this->getUser() )
1057  : User::newFromName( $this->getUserText(), false );
1058  $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1059 
1060  // Username hidden?
1061  $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1062 
1063  $jconds = [
1064  'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1065  ] + $actorQuery['joins'];
1066 
1067  $options = [
1068  'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1069  'ORDER BY' => 'timestamp DESC',
1070  ];
1071 
1072  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1073  return new UserArrayFromResult( $res );
1074  }
1075 
1083  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1084  return $parserOptions->getStubThreshold() == 0
1085  && $this->exists()
1086  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1087  && $this->getContentHandler()->isParserCacheSupported();
1088  }
1089 
1103  public function getParserOutput(
1104  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1105  ) {
1106  $useParserCache =
1107  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1108 
1109  if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1110  throw new InvalidArgumentException(
1111  'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1112  );
1113  }
1114 
1115  wfDebug( __METHOD__ .
1116  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1117  if ( $parserOptions->getStubThreshold() ) {
1118  wfIncrStats( 'pcache.miss.stub' );
1119  }
1120 
1121  if ( $useParserCache ) {
1122  $parserOutput = MediaWikiServices::getInstance()->getParserCache()
1123  ->get( $this, $parserOptions );
1124  if ( $parserOutput !== false ) {
1125  return $parserOutput;
1126  }
1127  }
1128 
1129  if ( $oldid === null || $oldid === 0 ) {
1130  $oldid = $this->getLatest();
1131  }
1132 
1133  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1134  $pool->execute();
1135 
1136  return $pool->getParserOutput();
1137  }
1138 
1144  public function doViewUpdates( User $user, $oldid = 0 ) {
1145  if ( wfReadOnly() ) {
1146  return;
1147  }
1148 
1149  // Update newtalk / watchlist notification status;
1150  // Avoid outage if the master is not reachable by using a deferred updated
1152  function () use ( $user, $oldid ) {
1153  Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1154 
1155  $user->clearNotification( $this->mTitle, $oldid );
1156  },
1158  );
1159  }
1160 
1167  public function doPurge() {
1168  // Avoid PHP 7.1 warning of passing $this by reference
1169  $wikiPage = $this;
1170 
1171  if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1172  return false;
1173  }
1174 
1175  $this->mTitle->invalidateCache();
1176 
1177  // Clear file cache
1179  // Send purge after above page_touched update was committed
1181  new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1183  );
1184 
1185  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1186  $messageCache = MessageCache::singleton();
1187  $messageCache->updateMessageOverride( $this->mTitle, $this->getContent() );
1188  }
1189 
1190  return true;
1191  }
1192 
1207  public function insertOn( $dbw, $pageId = null ) {
1208  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1209  $dbw->insert(
1210  'page',
1211  [
1212  'page_namespace' => $this->mTitle->getNamespace(),
1213  'page_title' => $this->mTitle->getDBkey(),
1214  'page_restrictions' => '',
1215  'page_is_redirect' => 0, // Will set this shortly...
1216  'page_is_new' => 1,
1217  'page_random' => wfRandom(),
1218  'page_touched' => $dbw->timestamp(),
1219  'page_latest' => 0, // Fill this in shortly...
1220  'page_len' => 0, // Fill this in shortly...
1221  ] + $pageIdForInsert,
1222  __METHOD__,
1223  'IGNORE'
1224  );
1225 
1226  if ( $dbw->affectedRows() > 0 ) {
1227  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1228  $this->mId = $newid;
1229  $this->mTitle->resetArticleID( $newid );
1230 
1231  return $newid;
1232  } else {
1233  return false; // nothing changed
1234  }
1235  }
1236 
1250  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1251  $lastRevIsRedirect = null
1252  ) {
1254 
1255  // Assertion to try to catch T92046
1256  if ( (int)$revision->getId() === 0 ) {
1257  throw new InvalidArgumentException(
1258  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1259  );
1260  }
1261 
1262  $content = $revision->getContent();
1263  $len = $content ? $content->getSize() : 0;
1264  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1265 
1266  $conditions = [ 'page_id' => $this->getId() ];
1267 
1268  if ( !is_null( $lastRevision ) ) {
1269  // An extra check against threads stepping on each other
1270  $conditions['page_latest'] = $lastRevision;
1271  }
1272 
1273  $revId = $revision->getId();
1274  Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1275 
1276  $row = [ /* SET */
1277  'page_latest' => $revId,
1278  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1279  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1280  'page_is_redirect' => $rt !== null ? 1 : 0,
1281  'page_len' => $len,
1282  ];
1283 
1284  if ( $wgContentHandlerUseDB ) {
1285  $row['page_content_model'] = $revision->getContentModel();
1286  }
1287 
1288  $dbw->update( 'page',
1289  $row,
1290  $conditions,
1291  __METHOD__ );
1292 
1293  $result = $dbw->affectedRows() > 0;
1294  if ( $result ) {
1295  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1296  $this->setLastEdit( $revision );
1297  $this->mLatest = $revision->getId();
1298  $this->mIsRedirect = (bool)$rt;
1299  // Update the LinkCache.
1300  LinkCache::singleton()->addGoodLinkObj(
1301  $this->getId(),
1302  $this->mTitle,
1303  $len,
1304  $this->mIsRedirect,
1305  $this->mLatest,
1306  $revision->getContentModel()
1307  );
1308  }
1309 
1310  return $result;
1311  }
1312 
1324  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1325  // Always update redirects (target link might have changed)
1326  // Update/Insert if we don't know if the last revision was a redirect or not
1327  // Delete if changing from redirect to non-redirect
1328  $isRedirect = !is_null( $redirectTitle );
1329 
1330  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1331  return true;
1332  }
1333 
1334  if ( $isRedirect ) {
1335  $this->insertRedirectEntry( $redirectTitle );
1336  } else {
1337  // This is not a redirect, remove row from redirect table
1338  $where = [ 'rd_from' => $this->getId() ];
1339  $dbw->delete( 'redirect', $where, __METHOD__ );
1340  }
1341 
1342  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1343  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1344  }
1345 
1346  return ( $dbw->affectedRows() != 0 );
1347  }
1348 
1359  public function updateIfNewerOn( $dbw, $revision ) {
1360  $row = $dbw->selectRow(
1361  [ 'revision', 'page' ],
1362  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1363  [
1364  'page_id' => $this->getId(),
1365  'page_latest=rev_id' ],
1366  __METHOD__ );
1367 
1368  if ( $row ) {
1369  if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1370  return false;
1371  }
1372  $prev = $row->rev_id;
1373  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1374  } else {
1375  // No or missing previous revision; mark the page as new
1376  $prev = 0;
1377  $lastRevIsRedirect = null;
1378  }
1379 
1380  $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1381 
1382  return $ret;
1383  }
1384 
1395  public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
1396  $handler = $undo->getContentHandler();
1397  return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1398  }
1399 
1410  public function supportsSections() {
1411  return $this->getContentHandler()->supportsSections();
1412  }
1413 
1428  public function replaceSectionContent(
1429  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1430  ) {
1431  $baseRevId = null;
1432  if ( $edittime && $sectionId !== 'new' ) {
1433  $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
1434  $dbr = $lb->getConnection( DB_REPLICA );
1435  $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1436  // Try the master if this thread may have just added it.
1437  // This could be abstracted into a Revision method, but we don't want
1438  // to encourage loading of revisions by timestamp.
1439  if ( !$rev
1440  && $lb->getServerCount() > 1
1441  && $lb->hasOrMadeRecentMasterChanges()
1442  ) {
1443  $dbw = $lb->getConnection( DB_MASTER );
1444  $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1445  }
1446  if ( $rev ) {
1447  $baseRevId = $rev->getId();
1448  }
1449  }
1450 
1451  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1452  }
1453 
1467  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1468  $sectionTitle = '', $baseRevId = null
1469  ) {
1470  if ( strval( $sectionId ) === '' ) {
1471  // Whole-page edit; let the whole text through
1472  $newContent = $sectionContent;
1473  } else {
1474  if ( !$this->supportsSections() ) {
1475  throw new MWException( "sections not supported for content model " .
1476  $this->getContentHandler()->getModelID() );
1477  }
1478 
1479  // T32711: always use current version when adding a new section
1480  if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1481  $oldContent = $this->getContent();
1482  } else {
1483  $rev = Revision::newFromId( $baseRevId );
1484  if ( !$rev ) {
1485  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1486  $this->getId() . "; section: $sectionId)\n" );
1487  return null;
1488  }
1489 
1490  $oldContent = $rev->getContent();
1491  }
1492 
1493  if ( !$oldContent ) {
1494  wfDebug( __METHOD__ . ": no page text\n" );
1495  return null;
1496  }
1497 
1498  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1499  }
1500 
1501  return $newContent;
1502  }
1503 
1509  public function checkFlags( $flags ) {
1510  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1511  if ( $this->exists() ) {
1512  $flags |= EDIT_UPDATE;
1513  } else {
1514  $flags |= EDIT_NEW;
1515  }
1516  }
1517 
1518  return $flags;
1519  }
1520 
1579  public function doEditContent(
1580  Content $content, $summary, $flags = 0, $baseRevId = false,
1581  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1582  ) {
1584 
1585  // Old default parameter for $tags was null
1586  if ( $tags === null ) {
1587  $tags = [];
1588  }
1589 
1590  // Low-level sanity check
1591  if ( $this->mTitle->getText() === '' ) {
1592  throw new MWException( 'Something is trying to edit an article with an empty title' );
1593  }
1594  // Make sure the given content type is allowed for this page
1595  if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
1596  return Status::newFatal( 'content-not-allowed-here',
1598  $this->mTitle->getPrefixedText()
1599  );
1600  }
1601 
1602  // Load the data from the master database if needed.
1603  // The caller may already loaded it from the master or even loaded it using
1604  // SELECT FOR UPDATE, so do not override that using clear().
1605  $this->loadPageData( 'fromdbmaster' );
1606 
1607  $user = $user ?: $wgUser;
1608  $flags = $this->checkFlags( $flags );
1609 
1610  // Avoid PHP 7.1 warning of passing $this by reference
1611  $wikiPage = $this;
1612 
1613  // Trigger pre-save hook (using provided edit summary)
1614  $hookStatus = Status::newGood( [] );
1615  $hook_args = [ &$wikiPage, &$user, &$content, &$summary,
1616  $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
1617  // Check if the hook rejected the attempted save
1618  if ( !Hooks::run( 'PageContentSave', $hook_args ) ) {
1619  if ( $hookStatus->isOK() ) {
1620  // Hook returned false but didn't call fatal(); use generic message
1621  $hookStatus->fatal( 'edit-hook-aborted' );
1622  }
1623 
1624  return $hookStatus;
1625  }
1626 
1627  $old_revision = $this->getRevision(); // current revision
1628  $old_content = $this->getContent( Revision::RAW ); // current revision's content
1629 
1630  $handler = $content->getContentHandler();
1631  $tag = $handler->getChangeTag( $old_content, $content, $flags );
1632  // If there is no applicable tag, null is returned, so we need to check
1633  if ( $tag ) {
1634  $tags[] = $tag;
1635  }
1636 
1637  // Check for undo tag
1638  if ( $undidRevId !== 0 && in_array( 'mw-undo', ChangeTags::getSoftwareTags() ) ) {
1639  $tags[] = 'mw-undo';
1640  }
1641 
1642  // Provide autosummaries if summary is not provided and autosummaries are enabled
1643  if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
1644  $summary = $handler->getAutosummary( $old_content, $content, $flags );
1645  }
1646 
1647  // Avoid statsd noise and wasted cycles check the edit stash (T136678)
1648  if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
1649  $useCache = false;
1650  } else {
1651  $useCache = true;
1652  }
1653 
1654  // Get the pre-save transform content and final parser output
1655  $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat, $useCache );
1656  $pstContent = $editInfo->pstContent; // Content object
1657  $meta = [
1658  'bot' => ( $flags & EDIT_FORCE_BOT ),
1659  'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
1660  'serialized' => $pstContent->serialize( $serialFormat ),
1661  'serialFormat' => $serialFormat,
1662  'baseRevId' => $baseRevId,
1663  'oldRevision' => $old_revision,
1664  'oldContent' => $old_content,
1665  'oldId' => $this->getLatest(),
1666  'oldIsRedirect' => $this->isRedirect(),
1667  'oldCountable' => $this->isCountable(),
1668  'tags' => ( $tags !== null ) ? (array)$tags : [],
1669  'undidRevId' => $undidRevId
1670  ];
1671 
1672  // Actually create the revision and create/update the page
1673  if ( $flags & EDIT_UPDATE ) {
1674  $status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
1675  } else {
1676  $status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
1677  }
1678 
1679  // Promote user to any groups they meet the criteria for
1680  DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1681  $user->addAutopromoteOnceGroups( 'onEdit' );
1682  $user->addAutopromoteOnceGroups( 'onView' ); // b/c
1683  } );
1684 
1685  return $status;
1686  }
1687 
1700  private function doModify(
1701  Content $content, $flags, User $user, $summary, array $meta
1702  ) {
1704 
1705  // Update article, but only if changed.
1706  $status = Status::newGood( [ 'new' => false, 'revision' => null ] );
1707 
1708  // Convenience variables
1709  $now = wfTimestampNow();
1710  $oldid = $meta['oldId'];
1712  $oldContent = $meta['oldContent'];
1713  $newsize = $content->getSize();
1714 
1715  if ( !$oldid ) {
1716  // Article gone missing
1717  $status->fatal( 'edit-gone-missing' );
1718 
1719  return $status;
1720  } elseif ( !$oldContent ) {
1721  // Sanity check for T39225
1722  throw new MWException( "Could not find text for current revision {$oldid}." );
1723  }
1724 
1725  $changed = !$content->equals( $oldContent );
1726 
1727  $dbw = wfGetDB( DB_MASTER );
1728 
1729  if ( $changed ) {
1730  // @TODO: pass content object?!
1731  $revision = new Revision( [
1732  'page' => $this->getId(),
1733  'title' => $this->mTitle, // for determining the default content model
1734  'comment' => $summary,
1735  'minor_edit' => $meta['minor'],
1736  'text' => $meta['serialized'],
1737  'len' => $newsize,
1738  'parent_id' => $oldid,
1739  'user' => $user->getId(),
1740  'user_text' => $user->getName(),
1741  'timestamp' => $now,
1742  'content_model' => $content->getModel(),
1743  'content_format' => $meta['serialFormat'],
1744  ] );
1745 
1746  $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
1747  $status->merge( $prepStatus );
1748  if ( !$status->isOK() ) {
1749  return $status;
1750  }
1751 
1752  $dbw->startAtomic( __METHOD__ );
1753  // Get the latest page_latest value while locking it.
1754  // Do a CAS style check to see if it's the same as when this method
1755  // started. If it changed then bail out before touching the DB.
1756  $latestNow = $this->lockAndGetLatest();
1757  if ( $latestNow != $oldid ) {
1758  $dbw->endAtomic( __METHOD__ );
1759  // Page updated or deleted in the mean time
1760  $status->fatal( 'edit-conflict' );
1761 
1762  return $status;
1763  }
1764 
1765  // At this point we are now comitted to returning an OK
1766  // status unless some DB query error or other exception comes up.
1767  // This way callers don't have to call rollback() if $status is bad
1768  // unless they actually try to catch exceptions (which is rare).
1769 
1770  // Save the revision text
1771  $revisionId = $revision->insertOn( $dbw );
1772  // Update page_latest and friends to reflect the new revision
1773  if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
1774  throw new MWException( "Failed to update page row to use new revision." );
1775  }
1776 
1777  $tags = $meta['tags'];
1778  Hooks::run( 'NewRevisionFromEditComplete',
1779  [ $this, $revision, $meta['baseRevId'], $user, &$tags ] );
1780 
1781  // Update recentchanges
1782  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1783  // Mark as patrolled if the user can do so
1784  $autopatrolled = $wgUseRCPatrol && !count(
1785  $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1786  // Add RC row to the DB
1788  $now,
1789  $this->mTitle,
1790  $revision->isMinor(),
1791  $user,
1792  $summary,
1793  $oldid,
1794  $this->getTimestamp(),
1795  $meta['bot'],
1796  '',
1797  $oldContent ? $oldContent->getSize() : 0,
1798  $newsize,
1799  $revisionId,
1800  $autopatrolled ? RecentChange::PRC_AUTOPATROLLED :
1802  $tags
1803  );
1804  }
1805 
1806  $user->incEditCount();
1807 
1808  $dbw->endAtomic( __METHOD__ );
1809  $this->mTimestamp = $now;
1810  } else {
1811  // T34948: revision ID must be set to page {{REVISIONID}} and
1812  // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
1813  // Since we don't insert a new revision into the database, the least
1814  // error-prone way is to reuse given old revision.
1815  $revision = $meta['oldRevision'];
1816  }
1817 
1818  if ( $changed ) {
1819  // Return the new revision to the caller
1820  $status->value['revision'] = $revision;
1821  } else {
1822  $status->warning( 'edit-no-change' );
1823  // Update page_touched as updateRevisionOn() was not called.
1824  // Other cache updates are managed in onArticleEdit() via doEditUpdates().
1825  $this->mTitle->invalidateCache( $now );
1826  }
1827 
1828  // Do secondary updates once the main changes have been committed...
1830  new AtomicSectionUpdate(
1831  $dbw,
1832  __METHOD__,
1833  function () use (
1834  $revision, &$user, $content, $summary, &$flags,
1835  $changed, $meta, &$status
1836  ) {
1837  // Update links tables, site stats, etc.
1838  $this->doEditUpdates(
1839  $revision,
1840  $user,
1841  [
1842  'changed' => $changed,
1843  'oldcountable' => $meta['oldCountable'],
1844  'oldrevision' => $meta['oldRevision']
1845  ]
1846  );
1847  // Avoid PHP 7.1 warning of passing $this by reference
1848  $wikiPage = $this;
1849  // Trigger post-save hook
1850  $params = [ &$wikiPage, &$user, $content, $summary, $flags & EDIT_MINOR,
1851  null, null, &$flags, $revision, &$status, $meta['baseRevId'],
1852  $meta['undidRevId'] ];
1853  Hooks::run( 'PageContentSaveComplete', $params );
1854  }
1855  ),
1857  );
1858 
1859  return $status;
1860  }
1861 
1874  private function doCreate(
1875  Content $content, $flags, User $user, $summary, array $meta
1876  ) {
1878 
1879  $status = Status::newGood( [ 'new' => true, 'revision' => null ] );
1880 
1881  $now = wfTimestampNow();
1882  $newsize = $content->getSize();
1883  $prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
1884  $status->merge( $prepStatus );
1885  if ( !$status->isOK() ) {
1886  return $status;
1887  }
1888 
1889  $dbw = wfGetDB( DB_MASTER );
1890  $dbw->startAtomic( __METHOD__ );
1891 
1892  // Add the page record unless one already exists for the title
1893  $newid = $this->insertOn( $dbw );
1894  if ( $newid === false ) {
1895  $dbw->endAtomic( __METHOD__ ); // nothing inserted
1896  $status->fatal( 'edit-already-exists' );
1897 
1898  return $status; // nothing done
1899  }
1900 
1901  // At this point we are now comitted to returning an OK
1902  // status unless some DB query error or other exception comes up.
1903  // This way callers don't have to call rollback() if $status is bad
1904  // unless they actually try to catch exceptions (which is rare).
1905 
1906  // @TODO: pass content object?!
1907  $revision = new Revision( [
1908  'page' => $newid,
1909  'title' => $this->mTitle, // for determining the default content model
1910  'comment' => $summary,
1911  'minor_edit' => $meta['minor'],
1912  'text' => $meta['serialized'],
1913  'len' => $newsize,
1914  'user' => $user->getId(),
1915  'user_text' => $user->getName(),
1916  'timestamp' => $now,
1917  'content_model' => $content->getModel(),
1918  'content_format' => $meta['serialFormat'],
1919  ] );
1920 
1921  // Save the revision text...
1922  $revisionId = $revision->insertOn( $dbw );
1923  // Update the page record with revision data
1924  if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
1925  throw new MWException( "Failed to update page row to use new revision." );
1926  }
1927 
1928  Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
1929 
1930  // Update recentchanges
1931  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1932  // Mark as patrolled if the user can do so
1933  $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
1934  !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1935  // Add RC row to the DB
1937  $now,
1938  $this->mTitle,
1939  $revision->isMinor(),
1940  $user,
1941  $summary,
1942  $meta['bot'],
1943  '',
1944  $newsize,
1945  $revisionId,
1946  $patrolled,
1947  $meta['tags']
1948  );
1949  }
1950 
1951  $user->incEditCount();
1952 
1953  $dbw->endAtomic( __METHOD__ );
1954  $this->mTimestamp = $now;
1955 
1956  // Return the new revision to the caller
1957  $status->value['revision'] = $revision;
1958 
1959  // Do secondary updates once the main changes have been committed...
1961  new AtomicSectionUpdate(
1962  $dbw,
1963  __METHOD__,
1964  function () use (
1965  $revision, &$user, $content, $summary, &$flags, $meta, &$status
1966  ) {
1967  // Update links, etc.
1968  $this->doEditUpdates( $revision, $user, [ 'created' => true ] );
1969  // Avoid PHP 7.1 warning of passing $this by reference
1970  $wikiPage = $this;
1971  // Trigger post-create hook
1972  $params = [ &$wikiPage, &$user, $content, $summary,
1973  $flags & EDIT_MINOR, null, null, &$flags, $revision ];
1974  Hooks::run( 'PageContentInsertComplete', $params );
1975  // Trigger post-save hook
1976  $params = array_merge( $params, [ &$status, $meta['baseRevId'], 0 ] );
1977  Hooks::run( 'PageContentSaveComplete', $params );
1978  }
1979  ),
1981  );
1982 
1983  return $status;
1984  }
1985 
2000  public function makeParserOptions( $context ) {
2001  $options = $this->getContentHandler()->makeParserOptions( $context );
2002 
2003  if ( $this->getTitle()->isConversionTable() ) {
2004  // @todo ConversionTable should become a separate content model, so
2005  // we don't need special cases like this one.
2006  $options->disableContentConversion();
2007  }
2008 
2009  return $options;
2010  }
2011 
2029  public function prepareContentForEdit(
2030  Content $content, $revision = null, User $user = null,
2031  $serialFormat = null, $useCache = true
2032  ) {
2034 
2035  if ( is_object( $revision ) ) {
2036  $revid = $revision->getId();
2037  } else {
2038  $revid = $revision;
2039  // This code path is deprecated, and nothing is known to
2040  // use it, so performance here shouldn't be a worry.
2041  if ( $revid !== null ) {
2042  wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
2043  $revision = Revision::newFromId( $revid, Revision::READ_LATEST );
2044  } else {
2045  $revision = null;
2046  }
2047  }
2048 
2049  $user = is_null( $user ) ? $wgUser : $user;
2050  // XXX: check $user->getId() here???
2051 
2052  // Use a sane default for $serialFormat, see T59026
2053  if ( $serialFormat === null ) {
2054  $serialFormat = $content->getContentHandler()->getDefaultFormat();
2055  }
2056 
2057  if ( $this->mPreparedEdit
2058  && isset( $this->mPreparedEdit->newContent )
2059  && $this->mPreparedEdit->newContent->equals( $content )
2060  && $this->mPreparedEdit->revid == $revid
2061  && $this->mPreparedEdit->format == $serialFormat
2062  // XXX: also check $user here?
2063  ) {
2064  // Already prepared
2065  return $this->mPreparedEdit;
2066  }
2067 
2068  // The edit may have already been prepared via api.php?action=stashedit
2069  $cachedEdit = $useCache && $wgAjaxEditStash
2070  ? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
2071  : false;
2072 
2074  Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
2075 
2076  $edit = new PreparedEdit();
2077  if ( $cachedEdit ) {
2078  $edit->timestamp = $cachedEdit->timestamp;
2079  } else {
2080  $edit->timestamp = wfTimestampNow();
2081  }
2082  // @note: $cachedEdit is safely not used if the rev ID was referenced in the text
2083  $edit->revid = $revid;
2084 
2085  if ( $cachedEdit ) {
2086  $edit->pstContent = $cachedEdit->pstContent;
2087  } else {
2088  $edit->pstContent = $content
2089  ? $content->preSaveTransform( $this->mTitle, $user, $popts )
2090  : null;
2091  }
2092 
2093  $edit->format = $serialFormat;
2094  $edit->popts = $this->makeParserOptions( 'canonical' );
2095  if ( $cachedEdit ) {
2096  $edit->output = $cachedEdit->output;
2097  } else {
2098  if ( $revision ) {
2099  // We get here if vary-revision is set. This means that this page references
2100  // itself (such as via self-transclusion). In this case, we need to make sure
2101  // that any such self-references refer to the newly-saved revision, and not
2102  // to the previous one, which could otherwise happen due to replica DB lag.
2103  $oldCallback = $edit->popts->getCurrentRevisionCallback();
2104  $edit->popts->setCurrentRevisionCallback(
2105  function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
2106  if ( $title->equals( $revision->getTitle() ) ) {
2107  return $revision;
2108  } else {
2109  return call_user_func( $oldCallback, $title, $parser );
2110  }
2111  }
2112  );
2113  } else {
2114  // Try to avoid a second parse if {{REVISIONID}} is used
2115  $dbIndex = ( $this->mDataLoadedFrom & self::READ_LATEST ) === self::READ_LATEST
2116  ? DB_MASTER // use the best possible guess
2117  : DB_REPLICA; // T154554
2118 
2119  $edit->popts->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
2120  return 1 + (int)wfGetDB( $dbIndex )->selectField(
2121  'revision',
2122  'MAX(rev_id)',
2123  [],
2124  __METHOD__
2125  );
2126  } );
2127  }
2128  $edit->output = $edit->pstContent
2129  ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2130  : null;
2131  }
2132 
2133  $edit->newContent = $content;
2134  $edit->oldContent = $this->getContent( Revision::RAW );
2135 
2136  if ( $edit->output ) {
2137  $edit->output->setCacheTime( wfTimestampNow() );
2138  }
2139 
2140  // Process cache the result
2141  $this->mPreparedEdit = $edit;
2142 
2143  return $edit;
2144  }
2145 
2167  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2169 
2170  $options += [
2171  'changed' => true,
2172  'created' => false,
2173  'moved' => false,
2174  'restored' => false,
2175  'oldrevision' => null,
2176  'oldcountable' => null
2177  ];
2178  $content = $revision->getContent();
2179 
2180  $logger = LoggerFactory::getInstance( 'SaveParse' );
2181 
2182  // See if the parser output before $revision was inserted is still valid
2183  $editInfo = false;
2184  if ( !$this->mPreparedEdit ) {
2185  $logger->debug( __METHOD__ . ": No prepared edit...\n" );
2186  } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
2187  $logger->info( __METHOD__ . ": Prepared edit has vary-revision...\n" );
2188  } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision-id' )
2189  && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()
2190  ) {
2191  $logger->info( __METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n" );
2192  } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-user' ) && !$options['changed'] ) {
2193  $logger->info( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
2194  } else {
2195  wfDebug( __METHOD__ . ": Using prepared edit...\n" );
2196  $editInfo = $this->mPreparedEdit;
2197  }
2198 
2199  if ( !$editInfo ) {
2200  // Parse the text again if needed. Be careful not to do pre-save transform twice:
2201  // $text is usually already pre-save transformed once. Avoid using the edit stash
2202  // as any prepared content from there or in doEditContent() was already rejected.
2203  $editInfo = $this->prepareContentForEdit( $content, $revision, $user, null, false );
2204  }
2205 
2206  // Save it to the parser cache.
2207  // Make sure the cache time matches page_touched to avoid double parsing.
2208  MediaWikiServices::getInstance()->getParserCache()->save(
2209  $editInfo->output, $this, $editInfo->popts,
2210  $revision->getTimestamp(), $editInfo->revid
2211  );
2212 
2213  // Update the links tables and other secondary data
2214  if ( $content ) {
2215  $recursive = $options['changed']; // T52785
2216  $updates = $content->getSecondaryDataUpdates(
2217  $this->getTitle(), null, $recursive, $editInfo->output
2218  );
2219  foreach ( $updates as $update ) {
2220  $update->setCause( 'edit-page', $user->getName() );
2221  if ( $update instanceof LinksUpdate ) {
2222  $update->setRevision( $revision );
2223  $update->setTriggeringUser( $user );
2224  }
2225  DeferredUpdates::addUpdate( $update );
2226  }
2228  && $this->getContentHandler()->supportsCategories() === true
2229  && ( $options['changed'] || $options['created'] )
2230  && !$options['restored']
2231  ) {
2232  // Note: jobs are pushed after deferred updates, so the job should be able to see
2233  // the recent change entry (also done via deferred updates) and carry over any
2234  // bot/deletion/IP flags, ect.
2236  $this->getTitle(),
2237  [
2238  'pageId' => $this->getId(),
2239  'revTimestamp' => $revision->getTimestamp()
2240  ]
2241  ) );
2242  }
2243  }
2244 
2245  // Avoid PHP 7.1 warning of passing $this by reference
2246  $wikiPage = $this;
2247 
2248  Hooks::run( 'ArticleEditUpdates', [ &$wikiPage, &$editInfo, $options['changed'] ] );
2249 
2250  if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
2251  // Flush old entries from the `recentchanges` table
2252  if ( mt_rand( 0, 9 ) == 0 ) {
2254  }
2255  }
2256 
2257  if ( !$this->exists() ) {
2258  return;
2259  }
2260 
2261  $id = $this->getId();
2262  $title = $this->mTitle->getPrefixedDBkey();
2263  $shortTitle = $this->mTitle->getDBkey();
2264 
2265  if ( $options['oldcountable'] === 'no-change' ||
2266  ( !$options['changed'] && !$options['moved'] )
2267  ) {
2268  $good = 0;
2269  } elseif ( $options['created'] ) {
2270  $good = (int)$this->isCountable( $editInfo );
2271  } elseif ( $options['oldcountable'] !== null ) {
2272  $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
2273  } else {
2274  $good = 0;
2275  }
2276  $edits = $options['changed'] ? 1 : 0;
2277  $pages = $options['created'] ? 1 : 0;
2278 
2280  [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
2281  ) );
2282  DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
2283 
2284  // If this is another user's talk page, update newtalk.
2285  // Don't do this if $options['changed'] = false (null-edits) nor if
2286  // it's a minor edit and the user doesn't want notifications for those.
2287  if ( $options['changed']
2288  && $this->mTitle->getNamespace() == NS_USER_TALK
2289  && $shortTitle != $user->getTitleKey()
2290  && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
2291  ) {
2292  $recipient = User::newFromName( $shortTitle, false );
2293  if ( !$recipient ) {
2294  wfDebug( __METHOD__ . ": invalid username\n" );
2295  } else {
2296  // Avoid PHP 7.1 warning of passing $this by reference
2297  $wikiPage = $this;
2298 
2299  // Allow extensions to prevent user notification
2300  // when a new message is added to their talk page
2301  if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
2302  if ( User::isIP( $shortTitle ) ) {
2303  // An anonymous user
2304  $recipient->setNewtalk( true, $revision );
2305  } elseif ( $recipient->isLoggedIn() ) {
2306  $recipient->setNewtalk( true, $revision );
2307  } else {
2308  wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
2309  }
2310  }
2311  }
2312  }
2313 
2314  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
2315  MessageCache::singleton()->updateMessageOverride( $this->mTitle, $content );
2316  }
2317 
2318  if ( $options['created'] ) {
2319  self::onArticleCreate( $this->mTitle );
2320  } elseif ( $options['changed'] ) { // T52785
2321  self::onArticleEdit( $this->mTitle, $revision );
2322  }
2323 
2325  $this->mTitle, $options['oldrevision'], $revision, wfWikiID()
2326  );
2327  }
2328 
2343  public function doUpdateRestrictions( array $limit, array $expiry,
2344  &$cascade, $reason, User $user, $tags = null
2345  ) {
2347 
2348  if ( wfReadOnly() ) {
2349  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2350  }
2351 
2352  $this->loadPageData( 'fromdbmaster' );
2353  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2354  $id = $this->getId();
2355 
2356  if ( !$cascade ) {
2357  $cascade = false;
2358  }
2359 
2360  // Take this opportunity to purge out expired restrictions
2362 
2363  // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
2364  // we expect a single selection, but the schema allows otherwise.
2365  $isProtected = false;
2366  $protect = false;
2367  $changed = false;
2368 
2369  $dbw = wfGetDB( DB_MASTER );
2370 
2371  foreach ( $restrictionTypes as $action ) {
2372  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2373  $expiry[$action] = 'infinity';
2374  }
2375  if ( !isset( $limit[$action] ) ) {
2376  $limit[$action] = '';
2377  } elseif ( $limit[$action] != '' ) {
2378  $protect = true;
2379  }
2380 
2381  // Get current restrictions on $action
2382  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2383  if ( $current != '' ) {
2384  $isProtected = true;
2385  }
2386 
2387  if ( $limit[$action] != $current ) {
2388  $changed = true;
2389  } elseif ( $limit[$action] != '' ) {
2390  // Only check expiry change if the action is actually being
2391  // protected, since expiry does nothing on an not-protected
2392  // action.
2393  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2394  $changed = true;
2395  }
2396  }
2397  }
2398 
2399  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2400  $changed = true;
2401  }
2402 
2403  // If nothing has changed, do nothing
2404  if ( !$changed ) {
2405  return Status::newGood();
2406  }
2407 
2408  if ( !$protect ) { // No protection at all means unprotection
2409  $revCommentMsg = 'unprotectedarticle-comment';
2410  $logAction = 'unprotect';
2411  } elseif ( $isProtected ) {
2412  $revCommentMsg = 'modifiedarticleprotection-comment';
2413  $logAction = 'modify';
2414  } else {
2415  $revCommentMsg = 'protectedarticle-comment';
2416  $logAction = 'protect';
2417  }
2418 
2419  $logRelationsValues = [];
2420  $logRelationsField = null;
2421  $logParamsDetails = [];
2422 
2423  // Null revision (used for change tag insertion)
2424  $nullRevision = null;
2425 
2426  if ( $id ) { // Protection of existing page
2427  // Avoid PHP 7.1 warning of passing $this by reference
2428  $wikiPage = $this;
2429 
2430  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2431  return Status::newGood();
2432  }
2433 
2434  // Only certain restrictions can cascade...
2435  $editrestriction = isset( $limit['edit'] )
2436  ? [ $limit['edit'] ]
2437  : $this->mTitle->getRestrictions( 'edit' );
2438  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2439  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2440  }
2441  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2442  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2443  }
2444 
2445  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2446  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2447  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2448  }
2449  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2450  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2451  }
2452 
2453  // The schema allows multiple restrictions
2454  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2455  $cascade = false;
2456  }
2457 
2458  // insert null revision to identify the page protection change as edit summary
2459  $latest = $this->getLatest();
2460  $nullRevision = $this->insertProtectNullRevision(
2461  $revCommentMsg,
2462  $limit,
2463  $expiry,
2464  $cascade,
2465  $reason,
2466  $user
2467  );
2468 
2469  if ( $nullRevision === null ) {
2470  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2471  }
2472 
2473  $logRelationsField = 'pr_id';
2474 
2475  // Update restrictions table
2476  foreach ( $limit as $action => $restrictions ) {
2477  $dbw->delete(
2478  'page_restrictions',
2479  [
2480  'pr_page' => $id,
2481  'pr_type' => $action
2482  ],
2483  __METHOD__
2484  );
2485  if ( $restrictions != '' ) {
2486  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2487  $dbw->insert(
2488  'page_restrictions',
2489  [
2490  'pr_page' => $id,
2491  'pr_type' => $action,
2492  'pr_level' => $restrictions,
2493  'pr_cascade' => $cascadeValue,
2494  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2495  ],
2496  __METHOD__
2497  );
2498  $logRelationsValues[] = $dbw->insertId();
2499  $logParamsDetails[] = [
2500  'type' => $action,
2501  'level' => $restrictions,
2502  'expiry' => $expiry[$action],
2503  'cascade' => (bool)$cascadeValue,
2504  ];
2505  }
2506  }
2507 
2508  // Clear out legacy restriction fields
2509  $dbw->update(
2510  'page',
2511  [ 'page_restrictions' => '' ],
2512  [ 'page_id' => $id ],
2513  __METHOD__
2514  );
2515 
2516  // Avoid PHP 7.1 warning of passing $this by reference
2517  $wikiPage = $this;
2518 
2519  Hooks::run( 'NewRevisionFromEditComplete',
2520  [ $this, $nullRevision, $latest, $user ] );
2521  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2522  } else { // Protection of non-existing page (also known as "title protection")
2523  // Cascade protection is meaningless in this case
2524  $cascade = false;
2525 
2526  if ( $limit['create'] != '' ) {
2527  $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2528  $dbw->replace( 'protected_titles',
2529  [ [ 'pt_namespace', 'pt_title' ] ],
2530  [
2531  'pt_namespace' => $this->mTitle->getNamespace(),
2532  'pt_title' => $this->mTitle->getDBkey(),
2533  'pt_create_perm' => $limit['create'],
2534  'pt_timestamp' => $dbw->timestamp(),
2535  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2536  'pt_user' => $user->getId(),
2537  ] + $commentFields, __METHOD__
2538  );
2539  $logParamsDetails[] = [
2540  'type' => 'create',
2541  'level' => $limit['create'],
2542  'expiry' => $expiry['create'],
2543  ];
2544  } else {
2545  $dbw->delete( 'protected_titles',
2546  [
2547  'pt_namespace' => $this->mTitle->getNamespace(),
2548  'pt_title' => $this->mTitle->getDBkey()
2549  ], __METHOD__
2550  );
2551  }
2552  }
2553 
2554  $this->mTitle->flushRestrictions();
2555  InfoAction::invalidateCache( $this->mTitle );
2556 
2557  if ( $logAction == 'unprotect' ) {
2558  $params = [];
2559  } else {
2560  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2561  $params = [
2562  '4::description' => $protectDescriptionLog, // parameter for IRC
2563  '5:bool:cascade' => $cascade,
2564  'details' => $logParamsDetails, // parameter for localize and api
2565  ];
2566  }
2567 
2568  // Update the protection log
2569  $logEntry = new ManualLogEntry( 'protect', $logAction );
2570  $logEntry->setTarget( $this->mTitle );
2571  $logEntry->setComment( $reason );
2572  $logEntry->setPerformer( $user );
2573  $logEntry->setParameters( $params );
2574  if ( !is_null( $nullRevision ) ) {
2575  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2576  }
2577  $logEntry->setTags( $tags );
2578  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2579  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2580  }
2581  $logId = $logEntry->insert();
2582  $logEntry->publish( $logId );
2583 
2584  return Status::newGood( $logId );
2585  }
2586 
2598  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2599  array $expiry, $cascade, $reason, $user = null
2600  ) {
2601  $dbw = wfGetDB( DB_MASTER );
2602 
2603  // Prepare a null revision to be added to the history
2604  $editComment = wfMessage(
2605  $revCommentMsg,
2606  $this->mTitle->getPrefixedText(),
2607  $user ? $user->getName() : ''
2608  )->inContentLanguage()->text();
2609  if ( $reason ) {
2610  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2611  }
2612  $protectDescription = $this->protectDescription( $limit, $expiry );
2613  if ( $protectDescription ) {
2614  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2615  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2616  ->inContentLanguage()->text();
2617  }
2618  if ( $cascade ) {
2619  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2620  $editComment .= wfMessage( 'brackets' )->params(
2621  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2622  )->inContentLanguage()->text();
2623  }
2624 
2625  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2626  if ( $nullRev ) {
2627  $nullRev->insertOn( $dbw );
2628 
2629  // Update page record and touch page
2630  $oldLatest = $nullRev->getParentId();
2631  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2632  }
2633 
2634  return $nullRev;
2635  }
2636 
2641  protected function formatExpiry( $expiry ) {
2643 
2644  if ( $expiry != 'infinity' ) {
2645  return wfMessage(
2646  'protect-expiring',
2647  $wgContLang->timeanddate( $expiry, false, false ),
2648  $wgContLang->date( $expiry, false, false ),
2649  $wgContLang->time( $expiry, false, false )
2650  )->inContentLanguage()->text();
2651  } else {
2652  return wfMessage( 'protect-expiry-indefinite' )
2653  ->inContentLanguage()->text();
2654  }
2655  }
2656 
2664  public function protectDescription( array $limit, array $expiry ) {
2665  $protectDescription = '';
2666 
2667  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2668  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2669  # All possible message keys are listed here for easier grepping:
2670  # * restriction-create
2671  # * restriction-edit
2672  # * restriction-move
2673  # * restriction-upload
2674  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2675  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2676  # with '' filtered out. All possible message keys are listed below:
2677  # * protect-level-autoconfirmed
2678  # * protect-level-sysop
2679  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2680  ->inContentLanguage()->text();
2681 
2682  $expiryText = $this->formatExpiry( $expiry[$action] );
2683 
2684  if ( $protectDescription !== '' ) {
2685  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2686  }
2687  $protectDescription .= wfMessage( 'protect-summary-desc' )
2688  ->params( $actionText, $restrictionsText, $expiryText )
2689  ->inContentLanguage()->text();
2690  }
2691 
2692  return $protectDescription;
2693  }
2694 
2706  public function protectDescriptionLog( array $limit, array $expiry ) {
2708 
2709  $protectDescriptionLog = '';
2710 
2711  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2712  $expiryText = $this->formatExpiry( $expiry[$action] );
2713  $protectDescriptionLog .= $wgContLang->getDirMark() .
2714  "[$action=$restrictions] ($expiryText)";
2715  }
2716 
2717  return trim( $protectDescriptionLog );
2718  }
2719 
2729  protected static function flattenRestrictions( $limit ) {
2730  if ( !is_array( $limit ) ) {
2731  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2732  }
2733 
2734  $bits = [];
2735  ksort( $limit );
2736 
2737  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2738  $bits[] = "$action=$restrictions";
2739  }
2740 
2741  return implode( ':', $bits );
2742  }
2743 
2760  public function doDeleteArticle(
2761  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2762  ) {
2763  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
2764  return $status->isGood();
2765  }
2766 
2786  public function doDeleteArticleReal(
2787  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2788  $tags = [], $logsubtype = 'delete'
2789  ) {
2792 
2793  wfDebug( __METHOD__ . "\n" );
2794 
2796 
2797  if ( $this->mTitle->getDBkey() === '' ) {
2798  $status->error( 'cannotdelete',
2799  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2800  return $status;
2801  }
2802 
2803  // Avoid PHP 7.1 warning of passing $this by reference
2804  $wikiPage = $this;
2805 
2806  $deleter = is_null( $deleter ) ? $wgUser : $deleter;
2807  if ( !Hooks::run( 'ArticleDelete',
2808  [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2809  ) ) {
2810  if ( $status->isOK() ) {
2811  // Hook aborted but didn't set a fatal status
2812  $status->fatal( 'delete-hook-aborted' );
2813  }
2814  return $status;
2815  }
2816 
2817  $dbw = wfGetDB( DB_MASTER );
2818  $dbw->startAtomic( __METHOD__ );
2819 
2820  $this->loadPageData( self::READ_LATEST );
2821  $id = $this->getId();
2822  // T98706: lock the page from various other updates but avoid using
2823  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2824  // the revisions queries (which also JOIN on user). Only lock the page
2825  // row and CAS check on page_latest to see if the trx snapshot matches.
2826  $lockedLatest = $this->lockAndGetLatest();
2827  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2828  $dbw->endAtomic( __METHOD__ );
2829  // Page not there or trx snapshot is stale
2830  $status->error( 'cannotdelete',
2831  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2832  return $status;
2833  }
2834 
2835  // Given the lock above, we can be confident in the title and page ID values
2836  $namespace = $this->getTitle()->getNamespace();
2837  $dbKey = $this->getTitle()->getDBkey();
2838 
2839  // At this point we are now comitted to returning an OK
2840  // status unless some DB query error or other exception comes up.
2841  // This way callers don't have to call rollback() if $status is bad
2842  // unless they actually try to catch exceptions (which is rare).
2843 
2844  // we need to remember the old content so we can use it to generate all deletion updates.
2845  $revision = $this->getRevision();
2846  try {
2847  $content = $this->getContent( Revision::RAW );
2848  } catch ( Exception $ex ) {
2849  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2850  . $ex->getMessage() );
2851 
2852  $content = null;
2853  }
2854 
2855  $commentStore = CommentStore::getStore();
2856  $actorMigration = ActorMigration::newMigration();
2857 
2859  $bitfield = false;
2860 
2861  // Bitfields to further suppress the content
2862  if ( $suppress ) {
2863  $bitfield = Revision::SUPPRESSED_ALL;
2864  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2865  }
2866 
2867  // For now, shunt the revision data into the archive table.
2868  // Text is *not* removed from the text table; bulk storage
2869  // is left intact to avoid breaking block-compression or
2870  // immutable storage schemes.
2871  // In the future, we may keep revisions and mark them with
2872  // the rev_deleted field, which is reserved for this purpose.
2873 
2874  // Lock rows in `revision` and its temp tables, but not any others.
2875  // Note array_intersect() preserves keys from the first arg, and we're
2876  // assuming $revQuery has `revision` primary and isn't using subtables
2877  // for anything we care about.
2878  $res = $dbw->select(
2879  array_intersect(
2880  $revQuery['tables'],
2881  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2882  ),
2883  '1',
2884  [ 'rev_page' => $id ],
2885  __METHOD__,
2886  'FOR UPDATE',
2887  $revQuery['joins']
2888  );
2889  foreach ( $res as $row ) {
2890  // Fetch all rows in case the DB needs that to properly lock them.
2891  }
2892 
2893  // Get all of the page revisions
2894  $res = $dbw->select(
2895  $revQuery['tables'],
2896  $revQuery['fields'],
2897  [ 'rev_page' => $id ],
2898  __METHOD__,
2899  [],
2900  $revQuery['joins']
2901  );
2902 
2903  // Build their equivalent archive rows
2904  $rowsInsert = [];
2905  $revids = [];
2906 
2908  $ipRevIds = [];
2909 
2910  foreach ( $res as $row ) {
2911  $comment = $commentStore->getComment( 'rev_comment', $row );
2912  $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2913  $rowInsert = [
2914  'ar_namespace' => $namespace,
2915  'ar_title' => $dbKey,
2916  'ar_timestamp' => $row->rev_timestamp,
2917  'ar_minor_edit' => $row->rev_minor_edit,
2918  'ar_rev_id' => $row->rev_id,
2919  'ar_parent_id' => $row->rev_parent_id,
2920  'ar_text_id' => $row->rev_text_id,
2921  'ar_len' => $row->rev_len,
2922  'ar_page_id' => $id,
2923  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2924  'ar_sha1' => $row->rev_sha1,
2925  ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2926  + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2927  if ( $wgContentHandlerUseDB ) {
2928  $rowInsert['ar_content_model'] = $row->rev_content_model;
2929  $rowInsert['ar_content_format'] = $row->rev_content_format;
2930  }
2931  $rowsInsert[] = $rowInsert;
2932  $revids[] = $row->rev_id;
2933 
2934  // Keep track of IP edits, so that the corresponding rows can
2935  // be deleted in the ip_changes table.
2936  if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2937  $ipRevIds[] = $row->rev_id;
2938  }
2939  }
2940  // Copy them into the archive table
2941  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2942  // Save this so we can pass it to the ArticleDeleteComplete hook.
2943  $archivedRevisionCount = $dbw->affectedRows();
2944 
2945  // Clone the title and wikiPage, so we have the information we need when
2946  // we log and run the ArticleDeleteComplete hook.
2947  $logTitle = clone $this->mTitle;
2948  $wikiPageBeforeDelete = clone $this;
2949 
2950  // Now that it's safely backed up, delete it
2951  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2952  $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
2954  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2955  }
2957  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2958  }
2959 
2960  // Also delete records from ip_changes as applicable.
2961  if ( count( $ipRevIds ) > 0 ) {
2962  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2963  }
2964 
2965  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2966  $logtype = $suppress ? 'suppress' : 'delete';
2967 
2968  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2969  $logEntry->setPerformer( $deleter );
2970  $logEntry->setTarget( $logTitle );
2971  $logEntry->setComment( $reason );
2972  $logEntry->setTags( $tags );
2973  $logid = $logEntry->insert();
2974 
2975  $dbw->onTransactionPreCommitOrIdle(
2976  function () use ( $dbw, $logEntry, $logid ) {
2977  // T58776: avoid deadlocks (especially from FileDeleteForm)
2978  $logEntry->publish( $logid );
2979  },
2980  __METHOD__
2981  );
2982 
2983  $dbw->endAtomic( __METHOD__ );
2984 
2985  $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2986 
2987  Hooks::run( 'ArticleDeleteComplete', [
2988  &$wikiPageBeforeDelete,
2989  &$deleter,
2990  $reason,
2991  $id,
2992  $content,
2993  $logEntry,
2994  $archivedRevisionCount
2995  ] );
2996  $status->value = $logid;
2997 
2998  // Show log excerpt on 404 pages rather than just a link
2999  $cache = MediaWikiServices::getInstance()->getMainObjectStash();
3000  $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
3001  $cache->set( $key, 1, $cache::TTL_DAY );
3002 
3003  return $status;
3004  }
3005 
3012  public function lockAndGetLatest() {
3013  return (int)wfGetDB( DB_MASTER )->selectField(
3014  'page',
3015  'page_latest',
3016  [
3017  'page_id' => $this->getId(),
3018  // Typically page_id is enough, but some code might try to do
3019  // updates assuming the title is the same, so verify that
3020  'page_namespace' => $this->getTitle()->getNamespace(),
3021  'page_title' => $this->getTitle()->getDBkey()
3022  ],
3023  __METHOD__,
3024  [ 'FOR UPDATE' ]
3025  );
3026  }
3027 
3038  public function doDeleteUpdates(
3039  $id, Content $content = null, Revision $revision = null, User $user = null
3040  ) {
3041  try {
3042  $countable = $this->isCountable();
3043  } catch ( Exception $ex ) {
3044  // fallback for deleting broken pages for which we cannot load the content for
3045  // some reason. Note that doDeleteArticleReal() already logged this problem.
3046  $countable = false;
3047  }
3048 
3049  // Update site status
3051  [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3052  ) );
3053 
3054  // Delete pagelinks, update secondary indexes, etc
3055  $updates = $this->getDeletionUpdates( $content );
3056  foreach ( $updates as $update ) {
3057  DeferredUpdates::addUpdate( $update );
3058  }
3059 
3060  $causeAgent = $user ? $user->getName() : 'unknown';
3061  // Reparse any pages transcluding this page
3063  $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3064  // Reparse any pages including this image
3065  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3067  $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3068  }
3069 
3070  // Clear caches
3071  self::onArticleDelete( $this->mTitle );
3073  $this->mTitle, $revision, null, wfWikiID()
3074  );
3075 
3076  // Reset this object and the Title object
3077  $this->loadFromRow( false, self::READ_LATEST );
3078 
3079  // Search engine
3080  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3081  }
3082 
3112  public function doRollback(
3113  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3114  ) {
3115  $resultDetails = null;
3116 
3117  // Check permissions
3118  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3119  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3120  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3121 
3122  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3123  $errors[] = [ 'sessionfailure' ];
3124  }
3125 
3126  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3127  $errors[] = [ 'actionthrottledtext' ];
3128  }
3129 
3130  // If there were errors, bail out now
3131  if ( !empty( $errors ) ) {
3132  return $errors;
3133  }
3134 
3135  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3136  }
3137 
3158  public function commitRollback( $fromP, $summary, $bot,
3159  &$resultDetails, User $guser, $tags = null
3160  ) {
3162 
3163  $dbw = wfGetDB( DB_MASTER );
3164 
3165  if ( wfReadOnly() ) {
3166  return [ [ 'readonlytext' ] ];
3167  }
3168 
3169  // Get the last editor
3170  $current = $this->getRevision();
3171  if ( is_null( $current ) ) {
3172  // Something wrong... no page?
3173  return [ [ 'notanarticle' ] ];
3174  }
3175 
3176  $from = str_replace( '_', ' ', $fromP );
3177  // User name given should match up with the top revision.
3178  // If the user was deleted then $from should be empty.
3179  if ( $from != $current->getUserText() ) {
3180  $resultDetails = [ 'current' => $current ];
3181  return [ [ 'alreadyrolled',
3182  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3183  htmlspecialchars( $fromP ),
3184  htmlspecialchars( $current->getUserText() )
3185  ] ];
3186  }
3187 
3188  // Get the last edit not by this person...
3189  // Note: these may not be public values
3190  $userId = intval( $current->getUser( Revision::RAW ) );
3191  $userName = $current->getUserText( Revision::RAW );
3192  if ( $userId ) {
3193  $user = User::newFromId( $userId );
3194  $user->setName( $userName );
3195  } else {
3196  $user = User::newFromName( $current->getUserText( Revision::RAW ), false );
3197  }
3198 
3199  $actorWhere = ActorMigration::newMigration()->getWhere( $dbw, 'rev_user', $user );
3200 
3201  $s = $dbw->selectRow(
3202  [ 'revision' ] + $actorWhere['tables'],
3203  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3204  [
3205  'rev_page' => $current->getPage(),
3206  'NOT(' . $actorWhere['conds'] . ')',
3207  ],
3208  __METHOD__,
3209  [
3210  'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3211  'ORDER BY' => 'rev_timestamp DESC'
3212  ],
3213  $actorWhere['joins']
3214  );
3215  if ( $s === false ) {
3216  // No one else ever edited this page
3217  return [ [ 'cantrollback' ] ];
3218  } elseif ( $s->rev_deleted & Revision::DELETED_TEXT
3219  || $s->rev_deleted & Revision::DELETED_USER
3220  ) {
3221  // Only admins can see this text
3222  return [ [ 'notvisiblerev' ] ];
3223  }
3224 
3225  // Generate the edit summary if necessary
3226  $target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST );
3227  if ( empty( $summary ) ) {
3228  if ( $from == '' ) { // no public user name
3229  $summary = wfMessage( 'revertpage-nouser' );
3230  } else {
3231  $summary = wfMessage( 'revertpage' );
3232  }
3233  }
3234 
3235  // Allow the custom summary to use the same args as the default message
3236  $args = [
3237  $target->getUserText(), $from, $s->rev_id,
3238  $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3239  $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
3240  ];
3241  if ( $summary instanceof Message ) {
3242  $summary = $summary->params( $args )->inContentLanguage()->text();
3243  } else {
3244  $summary = wfMsgReplaceArgs( $summary, $args );
3245  }
3246 
3247  // Trim spaces on user supplied text
3248  $summary = trim( $summary );
3249 
3250  // Save
3251  $flags = EDIT_UPDATE | EDIT_INTERNAL;
3252 
3253  if ( $guser->isAllowed( 'minoredit' ) ) {
3254  $flags |= EDIT_MINOR;
3255  }
3256 
3257  if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3258  $flags |= EDIT_FORCE_BOT;
3259  }
3260 
3261  $targetContent = $target->getContent();
3262  $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
3263 
3264  if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3265  $tags[] = 'mw-rollback';
3266  }
3267 
3268  // Actually store the edit
3269  $status = $this->doEditContent(
3270  $targetContent,
3271  $summary,
3272  $flags,
3273  $target->getId(),
3274  $guser,
3275  null,
3276  $tags
3277  );
3278 
3279  // Set patrolling and bot flag on the edits, which gets rollbacked.
3280  // This is done even on edit failure to have patrolling in that case (T64157).
3281  $set = [];
3282  if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3283  // Mark all reverted edits as bot
3284  $set['rc_bot'] = 1;
3285  }
3286 
3287  if ( $wgUseRCPatrol ) {
3288  // Mark all reverted edits as patrolled
3289  $set['rc_patrolled'] = RecentChange::PRC_PATROLLED;
3290  }
3291 
3292  if ( count( $set ) ) {
3293  $actorWhere = ActorMigration::newMigration()->getWhere( $dbw, 'rc_user', $user, false );
3294  $dbw->update( 'recentchanges', $set,
3295  [ /* WHERE */
3296  'rc_cur_id' => $current->getPage(),
3297  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3298  $actorWhere['conds'], // No tables/joins are needed for rc_user
3299  ],
3300  __METHOD__
3301  );
3302  }
3303 
3304  if ( !$status->isOK() ) {
3305  return $status->getErrorsArray();
3306  }
3307 
3308  // raise error, when the edit is an edit without a new version
3309  $statusRev = isset( $status->value['revision'] )
3310  ? $status->value['revision']
3311  : null;
3312  if ( !( $statusRev instanceof Revision ) ) {
3313  $resultDetails = [ 'current' => $current ];
3314  return [ [ 'alreadyrolled',
3315  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3316  htmlspecialchars( $fromP ),
3317  htmlspecialchars( $current->getUserText() )
3318  ] ];
3319  }
3320 
3321  if ( $changingContentModel ) {
3322  // If the content model changed during the rollback,
3323  // make sure it gets logged to Special:Log/contentmodel
3324  $log = new ManualLogEntry( 'contentmodel', 'change' );
3325  $log->setPerformer( $guser );
3326  $log->setTarget( $this->mTitle );
3327  $log->setComment( $summary );
3328  $log->setParameters( [
3329  '4::oldmodel' => $current->getContentModel(),
3330  '5::newmodel' => $targetContent->getModel(),
3331  ] );
3332 
3333  $logId = $log->insert( $dbw );
3334  $log->publish( $logId );
3335  }
3336 
3337  $revId = $statusRev->getId();
3338 
3339  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3340 
3341  $resultDetails = [
3342  'summary' => $summary,
3343  'current' => $current,
3344  'target' => $target,
3345  'newid' => $revId,
3346  'tags' => $tags
3347  ];
3348 
3349  return [];
3350  }
3351 
3363  public static function onArticleCreate( Title $title ) {
3364  // Update existence markers on article/talk tabs...
3365  $other = $title->getOtherPage();
3366 
3367  $other->purgeSquid();
3368 
3369  $title->touchLinks();
3370  $title->purgeSquid();
3371  $title->deleteTitleProtection();
3372 
3373  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3374 
3375  // Invalidate caches of articles which include this page
3377  new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
3378  );
3379 
3380  if ( $title->getNamespace() == NS_CATEGORY ) {
3381  // Load the Category object, which will schedule a job to create
3382  // the category table row if necessary. Checking a replica DB is ok
3383  // here, in the worst case it'll run an unnecessary recount job on
3384  // a category that probably doesn't have many members.
3385  Category::newFromTitle( $title )->getID();
3386  }
3387  }
3388 
3394  public static function onArticleDelete( Title $title ) {
3395  // Update existence markers on article/talk tabs...
3396  // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3397  BacklinkCache::get( $title )->clear();
3398  $other = $title->getOtherPage();
3399 
3400  $other->purgeSquid();
3401 
3402  $title->touchLinks();
3403  $title->purgeSquid();
3404 
3405  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3406 
3407  // File cache
3410 
3411  // Messages
3412  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3413  MessageCache::singleton()->updateMessageOverride( $title, null );
3414  }
3415 
3416  // Images
3417  if ( $title->getNamespace() == NS_FILE ) {
3419  new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
3420  );
3421  }
3422 
3423  // User talk pages
3424  if ( $title->getNamespace() == NS_USER_TALK ) {
3425  $user = User::newFromName( $title->getText(), false );
3426  if ( $user ) {
3427  $user->setNewtalk( false );
3428  }
3429  }
3430 
3431  // Image redirects
3432  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3433  }
3434 
3441  public static function onArticleEdit( Title $title, Revision $revision = null ) {
3442  // Invalidate caches of articles which include this page
3444  new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
3445  );
3446 
3447  // Invalidate the caches of all pages which redirect here
3449  new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
3450  );
3451 
3452  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3453 
3454  // Purge CDN for this page only
3455  $title->purgeSquid();
3456  // Clear file cache for this page only
3458 
3459  $revid = $revision ? $revision->getId() : null;
3460  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3462  } );
3463  }
3464 
3473  public function getCategories() {
3474  $id = $this->getId();
3475  if ( $id == 0 ) {
3476  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3477  }
3478 
3479  $dbr = wfGetDB( DB_REPLICA );
3480  $res = $dbr->select( 'categorylinks',
3481  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3482  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3483  // as not being aliases, and NS_CATEGORY is numeric
3484  [ 'cl_from' => $id ],
3485  __METHOD__ );
3486 
3487  return TitleArray::newFromResult( $res );
3488  }
3489 
3496  public function getHiddenCategories() {
3497  $result = [];
3498  $id = $this->getId();
3499 
3500  if ( $id == 0 ) {
3501  return [];
3502  }
3503 
3504  $dbr = wfGetDB( DB_REPLICA );
3505  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3506  [ 'cl_to' ],
3507  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3508  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3509  __METHOD__ );
3510 
3511  if ( $res !== false ) {
3512  foreach ( $res as $row ) {
3513  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3514  }
3515  }
3516 
3517  return $result;
3518  }
3519 
3527  public function getAutoDeleteReason( &$hasHistory ) {
3528  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3529  }
3530 
3541  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3542  $id = $id ?: $this->getId();
3543  $ns = $this->getTitle()->getNamespace();
3544 
3545  $addFields = [ 'cat_pages = cat_pages + 1' ];
3546  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3547  if ( $ns == NS_CATEGORY ) {
3548  $addFields[] = 'cat_subcats = cat_subcats + 1';
3549  $removeFields[] = 'cat_subcats = cat_subcats - 1';
3550  } elseif ( $ns == NS_FILE ) {
3551  $addFields[] = 'cat_files = cat_files + 1';
3552  $removeFields[] = 'cat_files = cat_files - 1';
3553  }
3554 
3555  $dbw = wfGetDB( DB_MASTER );
3556 
3557  if ( count( $added ) ) {
3558  $existingAdded = $dbw->selectFieldValues(
3559  'category',
3560  'cat_title',
3561  [ 'cat_title' => $added ],
3562  __METHOD__
3563  );
3564 
3565  // For category rows that already exist, do a plain
3566  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3567  // to avoid creating gaps in the cat_id sequence.
3568  if ( count( $existingAdded ) ) {
3569  $dbw->update(
3570  'category',
3571  $addFields,
3572  [ 'cat_title' => $existingAdded ],
3573  __METHOD__
3574  );
3575  }
3576 
3577  $missingAdded = array_diff( $added, $existingAdded );
3578  if ( count( $missingAdded ) ) {
3579  $insertRows = [];
3580  foreach ( $missingAdded as $cat ) {
3581  $insertRows[] = [
3582  'cat_title' => $cat,
3583  'cat_pages' => 1,
3584  'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
3585  'cat_files' => ( $ns == NS_FILE ) ? 1 : 0,
3586  ];
3587  }
3588  $dbw->upsert(
3589  'category',
3590  $insertRows,
3591  [ 'cat_title' ],
3592  $addFields,
3593  __METHOD__
3594  );
3595  }
3596  }
3597 
3598  if ( count( $deleted ) ) {
3599  $dbw->update(
3600  'category',
3601  $removeFields,
3602  [ 'cat_title' => $deleted ],
3603  __METHOD__
3604  );
3605  }
3606 
3607  foreach ( $added as $catName ) {
3608  $cat = Category::newFromName( $catName );
3609  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3610  }
3611 
3612  foreach ( $deleted as $catName ) {
3613  $cat = Category::newFromName( $catName );
3614  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3615  }
3616 
3617  // Refresh counts on categories that should be empty now, to
3618  // trigger possible deletion. Check master for the most
3619  // up-to-date cat_pages.
3620  if ( count( $deleted ) ) {
3621  $rows = $dbw->select(
3622  'category',
3623  [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
3624  [ 'cat_title' => $deleted, 'cat_pages <= 0' ],
3625  __METHOD__
3626  );
3627  foreach ( $rows as $row ) {
3628  $cat = Category::newFromRow( $row );
3629  // T166757: do the update after this DB commit
3630  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3631  $cat->refreshCounts();
3632  } );
3633  }
3634  }
3635  }
3636 
3643  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3644  if ( wfReadOnly() ) {
3645  return;
3646  }
3647 
3648  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3649  [ $this, $this->mTitle, $parserOutput ]
3650  ) ) {
3651  return;
3652  }
3653 
3654  $config = RequestContext::getMain()->getConfig();
3655 
3656  $params = [
3657  'isOpportunistic' => true,
3658  'rootJobTimestamp' => $parserOutput->getCacheTime()
3659  ];
3660 
3661  if ( $this->mTitle->areRestrictionsCascading() ) {
3662  // If the page is cascade protecting, the links should really be up-to-date
3663  JobQueueGroup::singleton()->lazyPush(
3664  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3665  );
3666  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3667  // Assume the output contains "dynamic" time/random based magic words.
3668  // Only update pages that expired due to dynamic content and NOT due to edits
3669  // to referenced templates/files. When the cache expires due to dynamic content,
3670  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3671  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3672  // template/file edit already triggered recursive RefreshLinksJob jobs.
3673  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3674  // If a page is uncacheable, do not keep spamming a job for it.
3675  // Although it would be de-duplicated, it would still waste I/O.
3677  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3678  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3679  if ( $cache->add( $key, time(), $ttl ) ) {
3680  JobQueueGroup::singleton()->lazyPush(
3681  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3682  );
3683  }
3684  }
3685  }
3686  }
3687 
3697  public function getDeletionUpdates( Content $content = null ) {
3698  if ( !$content ) {
3699  // load content object, which may be used to determine the necessary updates.
3700  // XXX: the content may not be needed to determine the updates.
3701  try {
3702  $content = $this->getContent( Revision::RAW );
3703  } catch ( Exception $ex ) {
3704  // If we can't load the content, something is wrong. Perhaps that's why
3705  // the user is trying to delete the page, so let's not fail in that case.
3706  // Note that doDeleteArticleReal() will already have logged an issue with
3707  // loading the content.
3708  }
3709  }
3710 
3711  if ( !$content ) {
3712  $updates = [];
3713  } else {
3714  $updates = $content->getDeletionUpdates( $this );
3715  }
3716 
3717  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3718  return $updates;
3719  }
3720 
3728  public function isLocal() {
3729  return true;
3730  }
3731 
3741  public function getWikiDisplayName() {
3743  return $wgSitename;
3744  }
3745 
3754  public function getSourceURL() {
3755  return $this->getTitle()->getCanonicalURL();
3756  }
3757 
3764  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3765 
3766  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle()->getTitleValue() );
3767  }
3768 }
WikiPage\getCategories
getCategories()
#-
Definition: WikiPage.php:3473
Content\getContentHandler
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
Revision\FOR_PUBLIC
const FOR_PUBLIC
Definition: Revision.php:55
Revision\DELETED_USER
const DELETED_USER
Definition: Revision.php:49
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:40
$wgUseAutomaticEditSummaries
$wgUseAutomaticEditSummaries
If user doesn't specify any edit summary when making a an edit, MediaWiki will try to automatically c...
Definition: DefaultSettings.php:6582
Revision\getTimestamp
getTimestamp()
Definition: Revision.php:1008
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:293
RecentChange\notifyNew
static notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot, $ip='', $size=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to page creation Note: the title object must be loaded w...
Definition: RecentChange.php:732
$user
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account $user
Definition: hooks.txt:244
Page
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:24
$wgUser
$wgUser
Definition: Setup.php:894
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:614
Revision\newKnownCurrent
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:1288
WikiPage\onArticleCreate
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:3363
WikiPage\loadPageData
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:427
RepoGroup\singleton
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:59
Revision\SUPPRESSED_ALL
const SUPPRESSED_ALL
Definition: Revision.php:52
WikiPage\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3527
false
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
WikiPage\getRedirectTarget
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:887
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:367
$tables
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition: hooks.txt:990
Revision\newFromId
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:114
WikiPage\clearCacheFields
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:255
Title\getFragment
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1517
$context
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:2604
WikiPage\getUndoContent
getUndoContent(Revision $undo, Revision $undoafter=null)
Get the content that needs to be saved in order to undo all revisions between $undo and $undoafter.
Definition: WikiPage.php:1395
WikiPage\updateRevisionOn
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1250
TitleArray\newFromResult
static newFromResult( $res)
Definition: TitleArray.php:40
HTMLFileCache\clearFileCache
static clearFileCache(Title $title)
Clear the file caches for a page for all actions.
Definition: HTMLFileCache.php:232
WikiPage\getUser
getUser( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:756
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:157
EDIT_INTERNAL
const EDIT_INTERNAL
Definition: Defines.php:160
captcha-old.count
count
Definition: captcha-old.py:249
text
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition: design.txt:12
WikiPage\doDeleteArticle
doDeleteArticle( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $user=null)
Same as doDeleteArticleReal(), but returns a simple boolean.
Definition: WikiPage.php:2760
$wgCommentTableSchemaMigrationStage
int $wgCommentTableSchemaMigrationStage
Comment table schema migration stage.
Definition: DefaultSettings.php:8815
WikiPage\hasViewableContent
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition: WikiPage.php:535
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:602
Revision\getContent
getContent( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision content if it's available to the specified audience.
Definition: Revision.php:929
$result
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. 'LanguageGetMagic':DEPRECATED! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) '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 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) '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! 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:1985
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1968
WikiPage\doViewUpdates
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1144
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1467
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1509
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
WikiPage\$mDataLoadedFrom
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:64
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Definition: DeferredUpdates.php:76
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:37
LinksUpdate
Class the manages updates of *_link tables as well as similar extension-managed tables.
Definition: LinksUpdate.php:34
StatusValue\newFatal
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
WikiPage\replaceSectionContent
replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle='', $edittime=null)
Definition: WikiPage.php:1428
WikiPage\getUserText
getUserText( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:794
PoolWorkArticleView
Definition: PoolWorkArticleView.php:22
NS_FILE
const NS_FILE
Definition: Defines.php:71
$params
$params
Definition: styleTest.css.php:40
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2000
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:995
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1250
wfMsgReplaceArgs
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
Definition: GlobalFunctions.php:1381
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:591
WikiPage\getRevision
getRevision()
Get the latest revision.
Definition: WikiPage.php:697
RefreshLinksJob\newDynamic
static newDynamic(Title $title, array $params)
Definition: RefreshLinksJob.php:76
RefreshLinksJob\newPrioritized
static newPrioritized(Title $title, array $params)
Definition: RefreshLinksJob.php:64
Revision\getContentHandler
getContentHandler()
Returns the content handler appropriate for this revision's content model.
Definition: Revision.php:1001
User\newFromAnyId
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:657
$s
$s
Definition: mergeMessageFileList.php:187
WikiPage\getContributors
getContributors()
Get a list of users who have edited this article, not including the user who made the most recent rev...
Definition: WikiPage.php:1033
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1138
RecentChange\notifyEdit
static notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to an edit.
Definition: RecentChange.php:658
$res
$res
Definition: database.txt:21
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
WikiPage\getComment
getComment( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:812
Revision\newFromPageId
static newFromPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
Definition: Revision.php:152
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
Revision\getId
getId()
Get revision ID.
Definition: Revision.php:617
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
Definition: FakeResultWrapper.php:11
$wgContentHandlerUseDB
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility.
Definition: DefaultSettings.php:8525
Revision\insertOn
insertOn( $dbw)
Insert a new revision into the database, returning the new revision ID number on success and dies hor...
Definition: Revision.php:1109
WikiPage\onArticleEdit
static onArticleEdit(Title $title, Revision $revision=null)
Purge caches on page update etc.
Definition: WikiPage.php:3441
Content\getSize
getSize()
Returns the content's nominal size in "bogo-bytes".
WikiPage\$mLatest
$mLatest
Definition: WikiPage.php:50
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:215
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:6804
$revQuery
$revQuery
Definition: testCompression.php:51
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:6839
WikiPage\flattenRestrictions
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:2729
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:89
WikiPage\$mTitle
Title $mTitle
Definition: WikiPage.php:43
php
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
WikiPage\$mTouched
string $mTouched
Definition: WikiPage.php:84
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
WikiPage\triggerOpportunisticLinksUpdate
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3643
WikiPage\protectDescription
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2664
$dbr
$dbr
Definition: testCompression.php:50
IDBAccessObject\READ_LOCKING
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:62
WikiPage\doDeleteArticleReal
doDeleteArticleReal( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $deleter=null, $tags=[], $logsubtype='delete')
Back-end article deletion Deletes the article with database consistency, writes logs,...
Definition: WikiPage.php:2786
Revision
Definition: Revision.php:41
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1410
WikiPage\doEditContent
doEditContent(Content $content, $summary, $flags=0, $baseRevId=false, User $user=null, $serialFormat=null, $tags=[], $undidRevId=0)
Change an existing article or create a new article.
Definition: WikiPage.php:1579
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:947
Revision\getQueryInfo
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object.
Definition: Revision.php:492
WikiCategoryPage
Special handling for category pages.
Definition: WikiCategoryPage.php:26
MWException
MediaWiki exception.
Definition: MWException.php:26
$title
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:934
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:115
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:826
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Definition: GlobalFunctions.php:1111
WikiPage\doRollback
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:3112
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:970
ParserOptions\newFromUserAndLang
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
Definition: ParserOptions.php:992
wfArrayDiff2
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
Definition: GlobalFunctions.php:111
BacklinkCache\get
static get(Title $title)
Create a new BacklinkCache or reuse any existing one.
Definition: BacklinkCache.php:113
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:464
wfIncrStats
wfIncrStats( $key, $count=1)
Increment a statistics counter.
Definition: GlobalFunctions.php:1240
WikiPage\selectFields
static selectFields()
Return the list of revision fields that should be selected to create a new page.
Definition: WikiPage.php:286
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2800
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:275
Title\getInterwiki
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:857
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1103
WikiPage\getId
getId()
Definition: WikiPage.php:510
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1207
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1083
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:236
wfTimestampOrNull
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Definition: GlobalFunctions.php:1984
WikiPage\getContent
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:718
WikiPage\exists
exists()
Definition: WikiPage.php:520
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:103
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3394
User\isIP
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:943
WikiPage\$mRedirectTarget
Title $mRedirectTarget
Definition: WikiPage.php:69
WikiPage\__construct
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:95
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:591
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:61
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:613
ChangeTags\getSoftwareTags
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:53
$parser
do that in ParserLimitReportFormat instead $parser
Definition: hooks.txt:2595
SiteStatsUpdate\factory
static factory(array $deltas)
Definition: SiteStatsUpdate.php:66
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:534
global
when a variable name is used in a it is silently declared as a new masking the global
Definition: design.txt:93
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1997
WikiPage\setTimestamp
setTimestamp( $ts)
Set the page timestamp (use only to avoid DB queries)
Definition: WikiPage.php:743
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:79
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:624
ParserOptions\getStubThreshold
getStubThreshold()
Thumb size preferred by the user.
Definition: ParserOptions.php:552
DB_MASTER
const DB_MASTER
Definition: defines.php:26
IDBAccessObject\READ_NONE
const READ_NONE
Definition: IDBAccessObject.php:70
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:982
WikiPage\doPurge
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1167
WikiPage\isCountable
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:843
list
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
MessageCache\singleton
static singleton()
Get the signleton instance of this class.
Definition: MessageCache.php:113
WikiPage\getContentModel
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:562
Category\newFromRow
static newFromRow( $row, $title=null)
Factory function, for constructing a Category object from a result set.
Definition: Category.php:179
WikiPage\pageDataFromTitle
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object's namespace and title using a sanitized title string.
Definition: WikiPage.php:397
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:3012
CategoryMembershipChangeJob
Job to add recent change entries mentioning category membership changes.
Definition: CategoryMembershipChangeJob.php:38
MIGRATION_OLD
const MIGRATION_OLD
Definition: Defines.php:293
$wgPageLanguageUseDB
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
Definition: DefaultSettings.php:8608
Category\newFromTitle
static newFromTitle( $title)
Factory function.
Definition: Category.php:146
WikiPage\updateIfNewerOn
updateIfNewerOn( $dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record.
Definition: WikiPage.php:1359
RecentChange\PRC_PATROLLED
const PRC_PATROLLED
Definition: RecentChange.php:78
Content\prepareSave
prepareSave(WikiPage $page, $flags, $parentRevId, User $user)
Prepare Content for saving.
AtomicSectionUpdate
Deferrable Update for closure/callback updates via IDatabase::doAtomicSection()
Definition: AtomicSectionUpdate.php:9
WikiPage\setLastEdit
setLastEdit(Revision $revision)
Set the latest revision.
Definition: WikiPage.php:688
HTMLCacheUpdate
Class to invalidate the HTML cache of all the pages linking to a given title.
Definition: HTMLCacheUpdate.php:29
Content\equals
equals(Content $that=null)
Returns true if this Content objects is conceptually equivalent to the given Content object.
wfWikiID
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
Definition: GlobalFunctions.php:2751
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:68
WikiPage\protectDescriptionLog
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2706
WikiPage\insertRedirect
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:926
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:154
ContentHandler\getLocalizedName
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
Definition: ContentHandler.php:355
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:53
CdnCacheUpdate
Handles purging appropriate CDN URLs given a title (or titles)
Definition: CdnCacheUpdate.php:30
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
$retval
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account incomplete not yet checked for validity & $retval
Definition: hooks.txt:244
WikiPage\getCreator
getCreator( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:775
Title\GAID_FOR_UPDATE
const GAID_FOR_UPDATE
Used to be GAID_FOR_UPDATE define.
Definition: Title.php:54
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:87
WikiPage\newFromID
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:153
WikiPage\insertProtectNullRevision
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2598
$wgSitename
$wgSitename
Name of the site.
Definition: DefaultSettings.php:79
WikiPage\getSourceURL
getSourceURL()
Get the source URL for the content on this page, typically the canonical URL, but may be a remote lin...
Definition: WikiPage.php:3754
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1631
$ret
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:1987
Revision\RAW
const RAW
Definition: Revision.php:57
WikiPage\commitRollback
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:3158
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3496
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:183
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:79
$handler
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:783
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:434
IP\isValid
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
WikiPage\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object.
Definition: WikiPage.php:325
WikiPage\getOldestRevision
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:635
WikiPage\loadLastEdit
loadLastEdit()
Loads everything except the text This isn't necessary for all uses, so it's only done if needed.
Definition: WikiPage.php:648
Wikimedia\Rdbms\DBUnexpectedError
Definition: DBUnexpectedError.php:27
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:984
Content\preSaveTransform
preSaveTransform(Title $title, User $user, ParserOptions $parserOptions)
Returns a Content object with pre-save transformations applied (or this object if no transformations ...
Content
Base interface for content objects.
Definition: Content.php:34
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:153
WikiPage\loadFromRow
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:470
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:5308
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2641
$args
if( $line===false) $args
Definition: cdb.php:64
Title
Represents a title within MediaWiki.
Definition: Title.php:39
EDIT_AUTOSUMMARY
const EDIT_AUTOSUMMARY
Definition: Defines.php:159
wfRandom
wfRandom()
Get a random decimal value between 0 and 1, in a way not likely to give duplicate values for any real...
Definition: GlobalFunctions.php:287
User\isAllowedAny
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3747
wfReadOnlyReason
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
Definition: GlobalFunctions.php:1263
$cache
$cache
Definition: mcc.php:33
$rows
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 $rows
Definition: hooks.txt:2604
WikiPage\doEditUpdates
doEditUpdates(Revision $revision, User $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2167
$options
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:1987
ObjectCache\getMainWANInstance
static getMainWANInstance()
Get the main WAN cache object.
Definition: ObjectCache.php:380
DeferredUpdates\PRESEND
const PRESEND
Definition: DeferredUpdates.php:60
$wgAjaxEditStash
$wgAjaxEditStash
Have clients send edits to be prepared when filling in edit summaries.
Definition: DefaultSettings.php:8157
LinkCache\singleton
static singleton()
Get an instance of this class.
Definition: LinkCache.php:67
WikiPage\$mId
int $mId
Definition: WikiPage.php:59
JobQueueGroup\singleton
static singleton( $wiki=false)
Definition: JobQueueGroup.php:72
WikiPage\$mIsRedirect
$mIsRedirect
Definition: WikiPage.php:49
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:77
$rev
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:1767
as
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
WikiPage\getWikiDisplayName
getWikiDisplayName()
The display name for the site this content come from.
Definition: WikiPage.php:3741
LinksUpdate\queueRecursiveJobsForTable
static queueRecursiveJobsForTable(Title $title, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
Definition: LinksUpdate.php:345
WikiPage\prepareContentForEdit
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:2029
Revision\loadFromTimestamp
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:291
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:195
WikiPage\updateCategoryCounts
updateCategoryCounts(array $added, array $deleted, $id=0)
Update all the appropriate counts in the category table, given that we've added the categories $added...
Definition: WikiPage.php:3541
RecentChangesUpdateJob\newPurgeJob
static newPurgeJob()
Definition: RecentChangesUpdateJob.php:45
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3763
InfoAction\invalidateCache
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:70
LoggerFactory
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
$source
$source
Definition: mwdoc-filter.php:46
Content\getModel
getModel()
Returns the ID of the content model used by this Content object.
ManualLogEntry
Class for creating log entries manually, to inject them into the database.
Definition: LogEntry.php:432
WikiPage\pageData
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:364
Revision\newNullRevision
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
Definition: Revision.php:1159
$status
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. '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:1255
MediaWiki\Edit\PreparedEdit
Represents information returned by WikiPage::prepareContentForEdit()
Definition: PreparedEdit.php:32
WikiFilePage
Special handling for file pages.
Definition: WikiFilePage.php:30
WikiPage\isLocal
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3728
wfMessage
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 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
Revision\isMinor
isMinor()
Definition: Revision.php:873
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1125
$wgArticleCountMethod
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article.
Definition: DefaultSettings.php:4431
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:73
Category\newFromName
static newFromName( $name)
Factory function.
Definition: Category.php:126
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:155
EDIT_SUPPRESS_RC
const EDIT_SUPPRESS_RC
Definition: Defines.php:156
ParserOptions\isSafeToCache
isSafeToCache()
Test whether these options are safe to cache.
Definition: ParserOptions.php:1330
WikiPage\insertRedirectEntry
insertRedirectEntry(Title $rt, $oldLatest=null)
Insert or update the redirect table entry for this page to indicate it redirects to $rt.
Definition: WikiPage.php:951
MediaWikiServices
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
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:729
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1324
WikiPage\doModify
doModify(Content $content, $flags, User $user, $summary, array $meta)
Definition: WikiPage.php:1700
WikiPage\$mLinksUpdated
string $mLinksUpdated
Definition: WikiPage.php:89
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:544
WikiPage\$mLastRevision
Revision $mLastRevision
Definition: WikiPage.php:74
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(Title $title, Revision $old=null, Revision $new=null, $wikiId)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:443
CommentStore\getStore
static getStore()
Definition: CommentStore.php:130
WikiPage\$mDataLoaded
$mDataLoaded
Definition: WikiPage.php:48
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:53
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:111
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:203
WikiPage\$mPreparedEdit
PreparedEdit $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:54
WikiPage\getDeletionUpdates
getDeletionUpdates(Content $content=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3697
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:411
WikiPage\doCreate
doCreate(Content $content, $flags, User $user, $summary, array $meta)
Definition: WikiPage.php:1874
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:228
ApiStashEdit\checkCache
static checkCache(Title $title, Content $content, User $user)
Check that a prepared edit is in cache and still up-to-date.
Definition: ApiStashEdit.php:269
Revision\DELETED_TEXT
const DELETED_TEXT
Definition: Revision.php:47
WikiPage\$mTimestamp
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition: WikiPage.php:79
Title\purgeExpiredRestrictions
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3289
array
the array() calling protocol came about after MediaWiki 1.4rc1.
$wgRCWatchCategoryMembership
$wgRCWatchCategoryMembership
Treat category membership changes as a RecentChange.
Definition: DefaultSettings.php:6794
WikiPage\doDeleteUpdates
doDeleteUpdates( $id, Content $content=null, Revision $revision=null, User $user=null)
Do some database updates after deletion.
Definition: WikiPage.php:3038
User\isAllowed
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3777
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:244
WikiPage\doUpdateRestrictions
doUpdateRestrictions(array $limit, array $expiry, &$cascade, $reason, User $user, $tags=null)
Update the article's restriction field, and leave a log entry.
Definition: WikiPage.php:2343
$wgContLang
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the content language as $wgContLang
Definition: design.txt:56
$type
$type
Definition: testCompression.php:48
$wgActorTableSchemaMigrationStage
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage.
Definition: DefaultSettings.php:8822