MediaWiki  1.30.0
WikiPage.php
Go to the documentation of this file.
1 <?php
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 
92  const PURGE_CDN_CACHE = 1;
95  const PURGE_ALL = 7;
96 
101  public function __construct( Title $title ) {
102  $this->mTitle = $title;
103  }
104 
109  public function __clone() {
110  $this->mTitle = clone $this->mTitle;
111  }
112 
121  public static function factory( Title $title ) {
122  $ns = $title->getNamespace();
123 
124  if ( $ns == NS_MEDIA ) {
125  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
126  } elseif ( $ns < 0 ) {
127  throw new MWException( "Invalid or virtual namespace $ns given." );
128  }
129 
130  $page = null;
131  if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
132  return $page;
133  }
134 
135  switch ( $ns ) {
136  case NS_FILE:
137  $page = new WikiFilePage( $title );
138  break;
139  case NS_CATEGORY:
140  $page = new WikiCategoryPage( $title );
141  break;
142  default:
143  $page = new WikiPage( $title );
144  }
145 
146  return $page;
147  }
148 
159  public static function newFromID( $id, $from = 'fromdb' ) {
160  // page ids are never 0 or negative, see T63166
161  if ( $id < 1 ) {
162  return null;
163  }
164 
165  $from = self::convertSelectType( $from );
166  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
167  $row = $db->selectRow(
168  'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
169  if ( !$row ) {
170  return null;
171  }
172  return self::newFromRow( $row, $from );
173  }
174 
186  public static function newFromRow( $row, $from = 'fromdb' ) {
187  $page = self::factory( Title::newFromRow( $row ) );
188  $page->loadFromRow( $row, $from );
189  return $page;
190  }
191 
198  private static function convertSelectType( $type ) {
199  switch ( $type ) {
200  case 'fromdb':
201  return self::READ_NORMAL;
202  case 'fromdbmaster':
203  return self::READ_LATEST;
204  case 'forupdate':
205  return self::READ_LOCKING;
206  default:
207  // It may already be an integer or whatever else
208  return $type;
209  }
210  }
211 
218  public function getActionOverrides() {
219  return $this->getContentHandler()->getActionOverrides();
220  }
221 
231  public function getContentHandler() {
233  }
234 
239  public function getTitle() {
240  return $this->mTitle;
241  }
242 
247  public function clear() {
248  $this->mDataLoaded = false;
249  $this->mDataLoadedFrom = self::READ_NONE;
250 
251  $this->clearCacheFields();
252  }
253 
258  protected function clearCacheFields() {
259  $this->mId = null;
260  $this->mRedirectTarget = null; // Title object if set
261  $this->mLastRevision = null; // Latest revision
262  $this->mTouched = '19700101000000';
263  $this->mLinksUpdated = '19700101000000';
264  $this->mTimestamp = '';
265  $this->mIsRedirect = false;
266  $this->mLatest = false;
267  // T59026: do not clear mPreparedEdit since prepareTextForEdit() already checks
268  // the requested rev ID and content against the cached one for equality. For most
269  // content types, the output should not change during the lifetime of this cache.
270  // Clearing it can cause extra parses on edit for no reason.
271  }
272 
278  public function clearPreparedEdit() {
279  $this->mPreparedEdit = false;
280  }
281 
288  public static function selectFields() {
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 
323  protected function pageData( $dbr, $conditions, $options = [] ) {
324  $fields = self::selectFields();
325 
326  // Avoid PHP 7.1 warning of passing $this by reference
327  $wikiPage = $this;
328 
329  Hooks::run( 'ArticlePageDataBefore', [ &$wikiPage, &$fields ] );
330 
331  $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
332 
333  Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
334 
335  return $row;
336  }
337 
347  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
348  return $this->pageData( $dbr, [
349  'page_namespace' => $title->getNamespace(),
350  'page_title' => $title->getDBkey() ], $options );
351  }
352 
361  public function pageDataFromId( $dbr, $id, $options = [] ) {
362  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
363  }
364 
377  public function loadPageData( $from = 'fromdb' ) {
378  $from = self::convertSelectType( $from );
379  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
380  // We already have the data from the correct location, no need to load it twice.
381  return;
382  }
383 
384  if ( is_int( $from ) ) {
385  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
386  $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
387  $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
388 
389  if ( !$data
390  && $index == DB_REPLICA
391  && $loadBalancer->getServerCount() > 1
392  && $loadBalancer->hasOrMadeRecentMasterChanges()
393  ) {
394  $from = self::READ_LATEST;
395  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
396  $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
397  }
398  } else {
399  // No idea from where the caller got this data, assume replica DB.
400  $data = $from;
401  $from = self::READ_NORMAL;
402  }
403 
404  $this->loadFromRow( $data, $from );
405  }
406 
418  public function loadFromRow( $data, $from ) {
419  $lc = LinkCache::singleton();
420  $lc->clearLink( $this->mTitle );
421 
422  if ( $data ) {
423  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
424 
425  $this->mTitle->loadFromRow( $data );
426 
427  // Old-fashioned restrictions
428  $this->mTitle->loadRestrictions( $data->page_restrictions );
429 
430  $this->mId = intval( $data->page_id );
431  $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
432  $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
433  $this->mIsRedirect = intval( $data->page_is_redirect );
434  $this->mLatest = intval( $data->page_latest );
435  // T39225: $latest may no longer match the cached latest Revision object.
436  // Double-check the ID of any cached latest Revision object for consistency.
437  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
438  $this->mLastRevision = null;
439  $this->mTimestamp = '';
440  }
441  } else {
442  $lc->addBadLinkObj( $this->mTitle );
443 
444  $this->mTitle->loadFromRow( false );
445 
446  $this->clearCacheFields();
447 
448  $this->mId = 0;
449  }
450 
451  $this->mDataLoaded = true;
452  $this->mDataLoadedFrom = self::convertSelectType( $from );
453  }
454 
458  public function getId() {
459  if ( !$this->mDataLoaded ) {
460  $this->loadPageData();
461  }
462  return $this->mId;
463  }
464 
468  public function exists() {
469  if ( !$this->mDataLoaded ) {
470  $this->loadPageData();
471  }
472  return $this->mId > 0;
473  }
474 
483  public function hasViewableContent() {
484  return $this->mTitle->isKnown();
485  }
486 
492  public function isRedirect() {
493  if ( !$this->mDataLoaded ) {
494  $this->loadPageData();
495  }
496 
497  return (bool)$this->mIsRedirect;
498  }
499 
510  public function getContentModel() {
511  if ( $this->exists() ) {
513 
514  return $cache->getWithSetCallback(
515  $cache->makeKey( 'page', 'content-model', $this->getLatest() ),
516  $cache::TTL_MONTH,
517  function () {
518  $rev = $this->getRevision();
519  if ( $rev ) {
520  // Look at the revision's actual content model
521  return $rev->getContentModel();
522  } else {
523  $title = $this->mTitle->getPrefixedDBkey();
524  wfWarn( "Page $title exists but has no (visible) revisions!" );
525  return $this->mTitle->getContentModel();
526  }
527  }
528  );
529  }
530 
531  // use the default model for this page
532  return $this->mTitle->getContentModel();
533  }
534 
539  public function checkTouched() {
540  if ( !$this->mDataLoaded ) {
541  $this->loadPageData();
542  }
543  return ( $this->mId && !$this->mIsRedirect );
544  }
545 
550  public function getTouched() {
551  if ( !$this->mDataLoaded ) {
552  $this->loadPageData();
553  }
554  return $this->mTouched;
555  }
556 
561  public function getLinksTimestamp() {
562  if ( !$this->mDataLoaded ) {
563  $this->loadPageData();
564  }
565  return $this->mLinksUpdated;
566  }
567 
572  public function getLatest() {
573  if ( !$this->mDataLoaded ) {
574  $this->loadPageData();
575  }
576  return (int)$this->mLatest;
577  }
578 
583  public function getOldestRevision() {
584  // Try using the replica DB first, then try the master
585  $rev = $this->mTitle->getFirstRevision();
586  if ( !$rev ) {
587  $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE );
588  }
589  return $rev;
590  }
591 
596  protected function loadLastEdit() {
597  if ( $this->mLastRevision !== null ) {
598  return; // already loaded
599  }
600 
601  $latest = $this->getLatest();
602  if ( !$latest ) {
603  return; // page doesn't exist or is missing page_latest info
604  }
605 
606  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
607  // T39225: if session S1 loads the page row FOR UPDATE, the result always
608  // includes the latest changes committed. This is true even within REPEATABLE-READ
609  // transactions, where S1 normally only sees changes committed before the first S1
610  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
611  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
612  // happened after the first S1 SELECT.
613  // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
615  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
616  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
617  // Bug T93976: if page_latest was loaded from the master, fetch the
618  // revision from there as well, as it may not exist yet on a replica DB.
619  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
620  $flags = Revision::READ_LATEST;
621  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
622  } else {
623  $dbr = wfGetDB( DB_REPLICA );
624  $revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
625  }
626 
627  if ( $revision ) { // sanity
628  $this->setLastEdit( $revision );
629  }
630  }
631 
636  protected function setLastEdit( Revision $revision ) {
637  $this->mLastRevision = $revision;
638  $this->mTimestamp = $revision->getTimestamp();
639  }
640 
645  public function getRevision() {
646  $this->loadLastEdit();
647  if ( $this->mLastRevision ) {
648  return $this->mLastRevision;
649  }
650  return null;
651  }
652 
666  public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
667  $this->loadLastEdit();
668  if ( $this->mLastRevision ) {
669  return $this->mLastRevision->getContent( $audience, $user );
670  }
671  return null;
672  }
673 
677  public function getTimestamp() {
678  // Check if the field has been filled by WikiPage::setTimestamp()
679  if ( !$this->mTimestamp ) {
680  $this->loadLastEdit();
681  }
682 
683  return wfTimestamp( TS_MW, $this->mTimestamp );
684  }
685 
691  public function setTimestamp( $ts ) {
692  $this->mTimestamp = wfTimestamp( TS_MW, $ts );
693  }
694 
704  public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
705  $this->loadLastEdit();
706  if ( $this->mLastRevision ) {
707  return $this->mLastRevision->getUser( $audience, $user );
708  } else {
709  return -1;
710  }
711  }
712 
723  public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
724  $revision = $this->getOldestRevision();
725  if ( $revision ) {
726  $userName = $revision->getUserText( $audience, $user );
727  return User::newFromName( $userName, false );
728  } else {
729  return null;
730  }
731  }
732 
742  public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
743  $this->loadLastEdit();
744  if ( $this->mLastRevision ) {
745  return $this->mLastRevision->getUserText( $audience, $user );
746  } else {
747  return '';
748  }
749  }
750 
760  public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
761  $this->loadLastEdit();
762  if ( $this->mLastRevision ) {
763  return $this->mLastRevision->getComment( $audience, $user );
764  } else {
765  return '';
766  }
767  }
768 
774  public function getMinorEdit() {
775  $this->loadLastEdit();
776  if ( $this->mLastRevision ) {
777  return $this->mLastRevision->isMinor();
778  } else {
779  return false;
780  }
781  }
782 
791  public function isCountable( $editInfo = false ) {
793 
794  if ( !$this->mTitle->isContentPage() ) {
795  return false;
796  }
797 
798  if ( $editInfo ) {
799  $content = $editInfo->pstContent;
800  } else {
801  $content = $this->getContent();
802  }
803 
804  if ( !$content || $content->isRedirect() ) {
805  return false;
806  }
807 
808  $hasLinks = null;
809 
810  if ( $wgArticleCountMethod === 'link' ) {
811  // nasty special case to avoid re-parsing to detect links
812 
813  if ( $editInfo ) {
814  // ParserOutput::getLinks() is a 2D array of page links, so
815  // to be really correct we would need to recurse in the array
816  // but the main array should only have items in it if there are
817  // links.
818  $hasLinks = (bool)count( $editInfo->output->getLinks() );
819  } else {
820  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
821  [ 'pl_from' => $this->getId() ], __METHOD__ );
822  }
823  }
824 
825  return $content->isCountable( $hasLinks );
826  }
827 
835  public function getRedirectTarget() {
836  if ( !$this->mTitle->isRedirect() ) {
837  return null;
838  }
839 
840  if ( $this->mRedirectTarget !== null ) {
841  return $this->mRedirectTarget;
842  }
843 
844  // Query the redirect table
845  $dbr = wfGetDB( DB_REPLICA );
846  $row = $dbr->selectRow( 'redirect',
847  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
848  [ 'rd_from' => $this->getId() ],
849  __METHOD__
850  );
851 
852  // rd_fragment and rd_interwiki were added later, populate them if empty
853  if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
854  $this->mRedirectTarget = Title::makeTitle(
855  $row->rd_namespace, $row->rd_title,
856  $row->rd_fragment, $row->rd_interwiki
857  );
858  return $this->mRedirectTarget;
859  }
860 
861  // This page doesn't have an entry in the redirect table
862  $this->mRedirectTarget = $this->insertRedirect();
863  return $this->mRedirectTarget;
864  }
865 
874  public function insertRedirect() {
875  $content = $this->getContent();
876  $retval = $content ? $content->getUltimateRedirectTarget() : null;
877  if ( !$retval ) {
878  return null;
879  }
880 
881  // Update the DB post-send if the page has not cached since now
882  $latest = $this->getLatest();
884  function () use ( $retval, $latest ) {
885  $this->insertRedirectEntry( $retval, $latest );
886  },
888  wfGetDB( DB_MASTER )
889  );
890 
891  return $retval;
892  }
893 
899  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
900  $dbw = wfGetDB( DB_MASTER );
901  $dbw->startAtomic( __METHOD__ );
902 
903  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
904  $dbw->upsert(
905  'redirect',
906  [
907  'rd_from' => $this->getId(),
908  'rd_namespace' => $rt->getNamespace(),
909  'rd_title' => $rt->getDBkey(),
910  'rd_fragment' => $rt->getFragment(),
911  'rd_interwiki' => $rt->getInterwiki(),
912  ],
913  [ 'rd_from' ],
914  [
915  'rd_namespace' => $rt->getNamespace(),
916  'rd_title' => $rt->getDBkey(),
917  'rd_fragment' => $rt->getFragment(),
918  'rd_interwiki' => $rt->getInterwiki(),
919  ],
920  __METHOD__
921  );
922  }
923 
924  $dbw->endAtomic( __METHOD__ );
925  }
926 
932  public function followRedirect() {
933  return $this->getRedirectURL( $this->getRedirectTarget() );
934  }
935 
943  public function getRedirectURL( $rt ) {
944  if ( !$rt ) {
945  return false;
946  }
947 
948  if ( $rt->isExternal() ) {
949  if ( $rt->isLocal() ) {
950  // Offsite wikis need an HTTP redirect.
951  // This can be hard to reverse and may produce loops,
952  // so they may be disabled in the site configuration.
953  $source = $this->mTitle->getFullURL( 'redirect=no' );
954  return $rt->getFullURL( [ 'rdfrom' => $source ] );
955  } else {
956  // External pages without "local" bit set are not valid
957  // redirect targets
958  return false;
959  }
960  }
961 
962  if ( $rt->isSpecialPage() ) {
963  // Gotta handle redirects to special pages differently:
964  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
965  // Some pages are not valid targets.
966  if ( $rt->isValidRedirectTarget() ) {
967  return $rt->getFullURL();
968  } else {
969  return false;
970  }
971  }
972 
973  return $rt;
974  }
975 
981  public function getContributors() {
982  // @todo FIXME: This is expensive; cache this info somewhere.
983 
984  $dbr = wfGetDB( DB_REPLICA );
985 
986  if ( $dbr->implicitGroupby() ) {
987  $realNameField = 'user_real_name';
988  } else {
989  $realNameField = 'MIN(user_real_name) AS user_real_name';
990  }
991 
992  $tables = [ 'revision', 'user' ];
993 
994  $fields = [
995  'user_id' => 'rev_user',
996  'user_name' => 'rev_user_text',
997  $realNameField,
998  'timestamp' => 'MAX(rev_timestamp)',
999  ];
1000 
1001  $conds = [ 'rev_page' => $this->getId() ];
1002 
1003  // The user who made the top revision gets credited as "this page was last edited by
1004  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1005  $user = $this->getUser();
1006  if ( $user ) {
1007  $conds[] = "rev_user != $user";
1008  } else {
1009  $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
1010  }
1011 
1012  // Username hidden?
1013  $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1014 
1015  $jconds = [
1016  'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
1017  ];
1018 
1019  $options = [
1020  'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
1021  'ORDER BY' => 'timestamp DESC',
1022  ];
1023 
1024  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1025  return new UserArrayFromResult( $res );
1026  }
1027 
1035  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1036  return $parserOptions->getStubThreshold() == 0
1037  && $this->exists()
1038  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1039  && $this->getContentHandler()->isParserCacheSupported();
1040  }
1041 
1055  public function getParserOutput(
1056  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1057  ) {
1058  $useParserCache =
1059  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1060 
1061  if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1062  throw new InvalidArgumentException(
1063  'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1064  );
1065  }
1066 
1067  wfDebug( __METHOD__ .
1068  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1069  if ( $parserOptions->getStubThreshold() ) {
1070  wfIncrStats( 'pcache.miss.stub' );
1071  }
1072 
1073  if ( $useParserCache ) {
1074  $parserOutput = MediaWikiServices::getInstance()->getParserCache()
1075  ->get( $this, $parserOptions );
1076  if ( $parserOutput !== false ) {
1077  return $parserOutput;
1078  }
1079  }
1080 
1081  if ( $oldid === null || $oldid === 0 ) {
1082  $oldid = $this->getLatest();
1083  }
1084 
1085  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1086  $pool->execute();
1087 
1088  return $pool->getParserOutput();
1089  }
1090 
1096  public function doViewUpdates( User $user, $oldid = 0 ) {
1097  if ( wfReadOnly() ) {
1098  return;
1099  }
1100 
1101  Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1102  // Update newtalk / watchlist notification status
1103  try {
1104  $user->clearNotification( $this->mTitle, $oldid );
1105  } catch ( DBError $e ) {
1106  // Avoid outage if the master is not reachable
1108  }
1109  }
1110 
1117  public function doPurge() {
1118  // Avoid PHP 7.1 warning of passing $this by reference
1119  $wikiPage = $this;
1120 
1121  if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1122  return false;
1123  }
1124 
1125  $this->mTitle->invalidateCache();
1126 
1127  // Clear file cache
1129  // Send purge after above page_touched update was committed
1131  new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1133  );
1134 
1135  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1136  $messageCache = MessageCache::singleton();
1137  $messageCache->updateMessageOverride( $this->mTitle, $this->getContent() );
1138  }
1139 
1140  return true;
1141  }
1142 
1150  public function getLastPurgeTimestamp() {
1151  wfDeprecated( __METHOD__, '1.29' );
1152  return false;
1153  }
1154 
1169  public function insertOn( $dbw, $pageId = null ) {
1170  $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1171  $dbw->insert(
1172  'page',
1173  [
1174  'page_namespace' => $this->mTitle->getNamespace(),
1175  'page_title' => $this->mTitle->getDBkey(),
1176  'page_restrictions' => '',
1177  'page_is_redirect' => 0, // Will set this shortly...
1178  'page_is_new' => 1,
1179  'page_random' => wfRandom(),
1180  'page_touched' => $dbw->timestamp(),
1181  'page_latest' => 0, // Fill this in shortly...
1182  'page_len' => 0, // Fill this in shortly...
1183  ] + $pageIdForInsert,
1184  __METHOD__,
1185  'IGNORE'
1186  );
1187 
1188  if ( $dbw->affectedRows() > 0 ) {
1189  $newid = $pageId ? (int)$pageId : $dbw->insertId();
1190  $this->mId = $newid;
1191  $this->mTitle->resetArticleID( $newid );
1192 
1193  return $newid;
1194  } else {
1195  return false; // nothing changed
1196  }
1197  }
1198 
1212  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1213  $lastRevIsRedirect = null
1214  ) {
1216 
1217  // Assertion to try to catch T92046
1218  if ( (int)$revision->getId() === 0 ) {
1219  throw new InvalidArgumentException(
1220  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1221  );
1222  }
1223 
1224  $content = $revision->getContent();
1225  $len = $content ? $content->getSize() : 0;
1226  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1227 
1228  $conditions = [ 'page_id' => $this->getId() ];
1229 
1230  if ( !is_null( $lastRevision ) ) {
1231  // An extra check against threads stepping on each other
1232  $conditions['page_latest'] = $lastRevision;
1233  }
1234 
1235  $row = [ /* SET */
1236  'page_latest' => $revision->getId(),
1237  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1238  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1239  'page_is_redirect' => $rt !== null ? 1 : 0,
1240  'page_len' => $len,
1241  ];
1242 
1243  if ( $wgContentHandlerUseDB ) {
1244  $row['page_content_model'] = $revision->getContentModel();
1245  }
1246 
1247  $dbw->update( 'page',
1248  $row,
1249  $conditions,
1250  __METHOD__ );
1251 
1252  $result = $dbw->affectedRows() > 0;
1253  if ( $result ) {
1254  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1255  $this->setLastEdit( $revision );
1256  $this->mLatest = $revision->getId();
1257  $this->mIsRedirect = (bool)$rt;
1258  // Update the LinkCache.
1259  LinkCache::singleton()->addGoodLinkObj(
1260  $this->getId(),
1261  $this->mTitle,
1262  $len,
1263  $this->mIsRedirect,
1264  $this->mLatest,
1265  $revision->getContentModel()
1266  );
1267  }
1268 
1269  return $result;
1270  }
1271 
1283  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1284  // Always update redirects (target link might have changed)
1285  // Update/Insert if we don't know if the last revision was a redirect or not
1286  // Delete if changing from redirect to non-redirect
1287  $isRedirect = !is_null( $redirectTitle );
1288 
1289  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1290  return true;
1291  }
1292 
1293  if ( $isRedirect ) {
1294  $this->insertRedirectEntry( $redirectTitle );
1295  } else {
1296  // This is not a redirect, remove row from redirect table
1297  $where = [ 'rd_from' => $this->getId() ];
1298  $dbw->delete( 'redirect', $where, __METHOD__ );
1299  }
1300 
1301  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1302  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1303  }
1304 
1305  return ( $dbw->affectedRows() != 0 );
1306  }
1307 
1318  public function updateIfNewerOn( $dbw, $revision ) {
1319  $row = $dbw->selectRow(
1320  [ 'revision', 'page' ],
1321  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1322  [
1323  'page_id' => $this->getId(),
1324  'page_latest=rev_id' ],
1325  __METHOD__ );
1326 
1327  if ( $row ) {
1328  if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1329  return false;
1330  }
1331  $prev = $row->rev_id;
1332  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1333  } else {
1334  // No or missing previous revision; mark the page as new
1335  $prev = 0;
1336  $lastRevIsRedirect = null;
1337  }
1338 
1339  $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1340 
1341  return $ret;
1342  }
1343 
1354  public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
1355  $handler = $undo->getContentHandler();
1356  return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1357  }
1358 
1369  public function supportsSections() {
1370  return $this->getContentHandler()->supportsSections();
1371  }
1372 
1387  public function replaceSectionContent(
1388  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1389  ) {
1390  $baseRevId = null;
1391  if ( $edittime && $sectionId !== 'new' ) {
1392  $dbr = wfGetDB( DB_REPLICA );
1393  $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1394  // Try the master if this thread may have just added it.
1395  // This could be abstracted into a Revision method, but we don't want
1396  // to encourage loading of revisions by timestamp.
1397  if ( !$rev
1398  && wfGetLB()->getServerCount() > 1
1399  && wfGetLB()->hasOrMadeRecentMasterChanges()
1400  ) {
1401  $dbw = wfGetDB( DB_MASTER );
1402  $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1403  }
1404  if ( $rev ) {
1405  $baseRevId = $rev->getId();
1406  }
1407  }
1408 
1409  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1410  }
1411 
1425  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1426  $sectionTitle = '', $baseRevId = null
1427  ) {
1428  if ( strval( $sectionId ) === '' ) {
1429  // Whole-page edit; let the whole text through
1430  $newContent = $sectionContent;
1431  } else {
1432  if ( !$this->supportsSections() ) {
1433  throw new MWException( "sections not supported for content model " .
1434  $this->getContentHandler()->getModelID() );
1435  }
1436 
1437  // T32711: always use current version when adding a new section
1438  if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1439  $oldContent = $this->getContent();
1440  } else {
1441  $rev = Revision::newFromId( $baseRevId );
1442  if ( !$rev ) {
1443  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1444  $this->getId() . "; section: $sectionId)\n" );
1445  return null;
1446  }
1447 
1448  $oldContent = $rev->getContent();
1449  }
1450 
1451  if ( !$oldContent ) {
1452  wfDebug( __METHOD__ . ": no page text\n" );
1453  return null;
1454  }
1455 
1456  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1457  }
1458 
1459  return $newContent;
1460  }
1461 
1467  public function checkFlags( $flags ) {
1468  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1469  if ( $this->exists() ) {
1470  $flags |= EDIT_UPDATE;
1471  } else {
1472  $flags |= EDIT_NEW;
1473  }
1474  }
1475 
1476  return $flags;
1477  }
1478 
1537  public function doEditContent(
1538  Content $content, $summary, $flags = 0, $baseRevId = false,
1539  User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1540  ) {
1542 
1543  // Old default parameter for $tags was null
1544  if ( $tags === null ) {
1545  $tags = [];
1546  }
1547 
1548  // Low-level sanity check
1549  if ( $this->mTitle->getText() === '' ) {
1550  throw new MWException( 'Something is trying to edit an article with an empty title' );
1551  }
1552  // Make sure the given content type is allowed for this page
1553  if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
1554  return Status::newFatal( 'content-not-allowed-here',
1556  $this->mTitle->getPrefixedText()
1557  );
1558  }
1559 
1560  // Load the data from the master database if needed.
1561  // The caller may already loaded it from the master or even loaded it using
1562  // SELECT FOR UPDATE, so do not override that using clear().
1563  $this->loadPageData( 'fromdbmaster' );
1564 
1565  $user = $user ?: $wgUser;
1566  $flags = $this->checkFlags( $flags );
1567 
1568  // Avoid PHP 7.1 warning of passing $this by reference
1569  $wikiPage = $this;
1570 
1571  // Trigger pre-save hook (using provided edit summary)
1572  $hookStatus = Status::newGood( [] );
1573  $hook_args = [ &$wikiPage, &$user, &$content, &$summary,
1574  $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
1575  // Check if the hook rejected the attempted save
1576  if ( !Hooks::run( 'PageContentSave', $hook_args ) ) {
1577  if ( $hookStatus->isOK() ) {
1578  // Hook returned false but didn't call fatal(); use generic message
1579  $hookStatus->fatal( 'edit-hook-aborted' );
1580  }
1581 
1582  return $hookStatus;
1583  }
1584 
1585  $old_revision = $this->getRevision(); // current revision
1586  $old_content = $this->getContent( Revision::RAW ); // current revision's content
1587 
1588  if ( $old_content && $old_content->getModel() !== $content->getModel() ) {
1589  $tags[] = 'mw-contentmodelchange';
1590  }
1591 
1592  // Provide autosummaries if one is not provided and autosummaries are enabled
1593  if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
1594  $handler = $content->getContentHandler();
1595  $summary = $handler->getAutosummary( $old_content, $content, $flags );
1596  }
1597 
1598  // Avoid statsd noise and wasted cycles check the edit stash (T136678)
1599  if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
1600  $useCache = false;
1601  } else {
1602  $useCache = true;
1603  }
1604 
1605  // Get the pre-save transform content and final parser output
1606  $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat, $useCache );
1607  $pstContent = $editInfo->pstContent; // Content object
1608  $meta = [
1609  'bot' => ( $flags & EDIT_FORCE_BOT ),
1610  'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
1611  'serialized' => $pstContent->serialize( $serialFormat ),
1612  'serialFormat' => $serialFormat,
1613  'baseRevId' => $baseRevId,
1614  'oldRevision' => $old_revision,
1615  'oldContent' => $old_content,
1616  'oldId' => $this->getLatest(),
1617  'oldIsRedirect' => $this->isRedirect(),
1618  'oldCountable' => $this->isCountable(),
1619  'tags' => ( $tags !== null ) ? (array)$tags : [],
1620  'undidRevId' => $undidRevId
1621  ];
1622 
1623  // Actually create the revision and create/update the page
1624  if ( $flags & EDIT_UPDATE ) {
1625  $status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
1626  } else {
1627  $status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
1628  }
1629 
1630  // Promote user to any groups they meet the criteria for
1631  DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1632  $user->addAutopromoteOnceGroups( 'onEdit' );
1633  $user->addAutopromoteOnceGroups( 'onView' ); // b/c
1634  } );
1635 
1636  return $status;
1637  }
1638 
1651  private function doModify(
1652  Content $content, $flags, User $user, $summary, array $meta
1653  ) {
1655 
1656  // Update article, but only if changed.
1657  $status = Status::newGood( [ 'new' => false, 'revision' => null ] );
1658 
1659  // Convenience variables
1660  $now = wfTimestampNow();
1661  $oldid = $meta['oldId'];
1663  $oldContent = $meta['oldContent'];
1664  $newsize = $content->getSize();
1665 
1666  if ( !$oldid ) {
1667  // Article gone missing
1668  $status->fatal( 'edit-gone-missing' );
1669 
1670  return $status;
1671  } elseif ( !$oldContent ) {
1672  // Sanity check for T39225
1673  throw new MWException( "Could not find text for current revision {$oldid}." );
1674  }
1675 
1676  // @TODO: pass content object?!
1677  $revision = new Revision( [
1678  'page' => $this->getId(),
1679  'title' => $this->mTitle, // for determining the default content model
1680  'comment' => $summary,
1681  'minor_edit' => $meta['minor'],
1682  'text' => $meta['serialized'],
1683  'len' => $newsize,
1684  'parent_id' => $oldid,
1685  'user' => $user->getId(),
1686  'user_text' => $user->getName(),
1687  'timestamp' => $now,
1688  'content_model' => $content->getModel(),
1689  'content_format' => $meta['serialFormat'],
1690  ] );
1691 
1692  $changed = !$content->equals( $oldContent );
1693 
1694  $dbw = wfGetDB( DB_MASTER );
1695 
1696  if ( $changed ) {
1697  $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
1698  $status->merge( $prepStatus );
1699  if ( !$status->isOK() ) {
1700  return $status;
1701  }
1702 
1703  $dbw->startAtomic( __METHOD__ );
1704  // Get the latest page_latest value while locking it.
1705  // Do a CAS style check to see if it's the same as when this method
1706  // started. If it changed then bail out before touching the DB.
1707  $latestNow = $this->lockAndGetLatest();
1708  if ( $latestNow != $oldid ) {
1709  $dbw->endAtomic( __METHOD__ );
1710  // Page updated or deleted in the mean time
1711  $status->fatal( 'edit-conflict' );
1712 
1713  return $status;
1714  }
1715 
1716  // At this point we are now comitted to returning an OK
1717  // status unless some DB query error or other exception comes up.
1718  // This way callers don't have to call rollback() if $status is bad
1719  // unless they actually try to catch exceptions (which is rare).
1720 
1721  // Save the revision text
1722  $revisionId = $revision->insertOn( $dbw );
1723  // Update page_latest and friends to reflect the new revision
1724  if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
1725  throw new MWException( "Failed to update page row to use new revision." );
1726  }
1727 
1728  Hooks::run( 'NewRevisionFromEditComplete',
1729  [ $this, $revision, $meta['baseRevId'], $user ] );
1730 
1731  // Update recentchanges
1732  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1733  // Mark as patrolled if the user can do so
1734  $patrolled = $wgUseRCPatrol && !count(
1735  $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1736  // Add RC row to the DB
1738  $now,
1739  $this->mTitle,
1740  $revision->isMinor(),
1741  $user,
1742  $summary,
1743  $oldid,
1744  $this->getTimestamp(),
1745  $meta['bot'],
1746  '',
1747  $oldContent ? $oldContent->getSize() : 0,
1748  $newsize,
1749  $revisionId,
1750  $patrolled,
1751  $meta['tags']
1752  );
1753  }
1754 
1755  $user->incEditCount();
1756 
1757  $dbw->endAtomic( __METHOD__ );
1758  $this->mTimestamp = $now;
1759  } else {
1760  // T34948: revision ID must be set to page {{REVISIONID}} and
1761  // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
1762  $revision->setId( $this->getLatest() );
1763  $revision->setUserIdAndName(
1764  $this->getUser( Revision::RAW ),
1765  $this->getUserText( Revision::RAW )
1766  );
1767  }
1768 
1769  if ( $changed ) {
1770  // Return the new revision to the caller
1771  $status->value['revision'] = $revision;
1772  } else {
1773  $status->warning( 'edit-no-change' );
1774  // Update page_touched as updateRevisionOn() was not called.
1775  // Other cache updates are managed in onArticleEdit() via doEditUpdates().
1776  $this->mTitle->invalidateCache( $now );
1777  }
1778 
1779  // Do secondary updates once the main changes have been committed...
1781  new AtomicSectionUpdate(
1782  $dbw,
1783  __METHOD__,
1784  function () use (
1785  $revision, &$user, $content, $summary, &$flags,
1786  $changed, $meta, &$status
1787  ) {
1788  // Update links tables, site stats, etc.
1789  $this->doEditUpdates(
1790  $revision,
1791  $user,
1792  [
1793  'changed' => $changed,
1794  'oldcountable' => $meta['oldCountable'],
1795  'oldrevision' => $meta['oldRevision']
1796  ]
1797  );
1798  // Avoid PHP 7.1 warning of passing $this by reference
1799  $wikiPage = $this;
1800  // Trigger post-save hook
1801  $params = [ &$wikiPage, &$user, $content, $summary, $flags & EDIT_MINOR,
1802  null, null, &$flags, $revision, &$status, $meta['baseRevId'],
1803  $meta['undidRevId'] ];
1804  Hooks::run( 'PageContentSaveComplete', $params );
1805  }
1806  ),
1808  );
1809 
1810  return $status;
1811  }
1812 
1825  private function doCreate(
1826  Content $content, $flags, User $user, $summary, array $meta
1827  ) {
1829 
1830  $status = Status::newGood( [ 'new' => true, 'revision' => null ] );
1831 
1832  $now = wfTimestampNow();
1833  $newsize = $content->getSize();
1834  $prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
1835  $status->merge( $prepStatus );
1836  if ( !$status->isOK() ) {
1837  return $status;
1838  }
1839 
1840  $dbw = wfGetDB( DB_MASTER );
1841  $dbw->startAtomic( __METHOD__ );
1842 
1843  // Add the page record unless one already exists for the title
1844  $newid = $this->insertOn( $dbw );
1845  if ( $newid === false ) {
1846  $dbw->endAtomic( __METHOD__ ); // nothing inserted
1847  $status->fatal( 'edit-already-exists' );
1848 
1849  return $status; // nothing done
1850  }
1851 
1852  // At this point we are now comitted to returning an OK
1853  // status unless some DB query error or other exception comes up.
1854  // This way callers don't have to call rollback() if $status is bad
1855  // unless they actually try to catch exceptions (which is rare).
1856 
1857  // @TODO: pass content object?!
1858  $revision = new Revision( [
1859  'page' => $newid,
1860  'title' => $this->mTitle, // for determining the default content model
1861  'comment' => $summary,
1862  'minor_edit' => $meta['minor'],
1863  'text' => $meta['serialized'],
1864  'len' => $newsize,
1865  'user' => $user->getId(),
1866  'user_text' => $user->getName(),
1867  'timestamp' => $now,
1868  'content_model' => $content->getModel(),
1869  'content_format' => $meta['serialFormat'],
1870  ] );
1871 
1872  // Save the revision text...
1873  $revisionId = $revision->insertOn( $dbw );
1874  // Update the page record with revision data
1875  if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
1876  throw new MWException( "Failed to update page row to use new revision." );
1877  }
1878 
1879  Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
1880 
1881  // Update recentchanges
1882  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1883  // Mark as patrolled if the user can do so
1884  $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
1885  !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1886  // Add RC row to the DB
1888  $now,
1889  $this->mTitle,
1890  $revision->isMinor(),
1891  $user,
1892  $summary,
1893  $meta['bot'],
1894  '',
1895  $newsize,
1896  $revisionId,
1897  $patrolled,
1898  $meta['tags']
1899  );
1900  }
1901 
1902  $user->incEditCount();
1903 
1904  $dbw->endAtomic( __METHOD__ );
1905  $this->mTimestamp = $now;
1906 
1907  // Return the new revision to the caller
1908  $status->value['revision'] = $revision;
1909 
1910  // Do secondary updates once the main changes have been committed...
1912  new AtomicSectionUpdate(
1913  $dbw,
1914  __METHOD__,
1915  function () use (
1916  $revision, &$user, $content, $summary, &$flags, $meta, &$status
1917  ) {
1918  // Update links, etc.
1919  $this->doEditUpdates( $revision, $user, [ 'created' => true ] );
1920  // Avoid PHP 7.1 warning of passing $this by reference
1921  $wikiPage = $this;
1922  // Trigger post-create hook
1923  $params = [ &$wikiPage, &$user, $content, $summary,
1924  $flags & EDIT_MINOR, null, null, &$flags, $revision ];
1925  Hooks::run( 'PageContentInsertComplete', $params );
1926  // Trigger post-save hook
1927  $params = array_merge( $params, [ &$status, $meta['baseRevId'], 0 ] );
1928  Hooks::run( 'PageContentSaveComplete', $params );
1929  }
1930  ),
1932  );
1933 
1934  return $status;
1935  }
1936 
1951  public function makeParserOptions( $context ) {
1952  $options = $this->getContentHandler()->makeParserOptions( $context );
1953 
1954  if ( $this->getTitle()->isConversionTable() ) {
1955  // @todo ConversionTable should become a separate content model, so
1956  // we don't need special cases like this one.
1957  $options->disableContentConversion();
1958  }
1959 
1960  return $options;
1961  }
1962 
1980  public function prepareContentForEdit(
1981  Content $content, $revision = null, User $user = null,
1982  $serialFormat = null, $useCache = true
1983  ) {
1985 
1986  if ( is_object( $revision ) ) {
1987  $revid = $revision->getId();
1988  } else {
1989  $revid = $revision;
1990  // This code path is deprecated, and nothing is known to
1991  // use it, so performance here shouldn't be a worry.
1992  if ( $revid !== null ) {
1993  wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
1994  $revision = Revision::newFromId( $revid, Revision::READ_LATEST );
1995  } else {
1996  $revision = null;
1997  }
1998  }
1999 
2000  $user = is_null( $user ) ? $wgUser : $user;
2001  // XXX: check $user->getId() here???
2002 
2003  // Use a sane default for $serialFormat, see T59026
2004  if ( $serialFormat === null ) {
2005  $serialFormat = $content->getContentHandler()->getDefaultFormat();
2006  }
2007 
2008  if ( $this->mPreparedEdit
2009  && isset( $this->mPreparedEdit->newContent )
2010  && $this->mPreparedEdit->newContent->equals( $content )
2011  && $this->mPreparedEdit->revid == $revid
2012  && $this->mPreparedEdit->format == $serialFormat
2013  // XXX: also check $user here?
2014  ) {
2015  // Already prepared
2016  return $this->mPreparedEdit;
2017  }
2018 
2019  // The edit may have already been prepared via api.php?action=stashedit
2020  $cachedEdit = $useCache && $wgAjaxEditStash
2021  ? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
2022  : false;
2023 
2025  Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
2026 
2027  $edit = new PreparedEdit();
2028  if ( $cachedEdit ) {
2029  $edit->timestamp = $cachedEdit->timestamp;
2030  } else {
2031  $edit->timestamp = wfTimestampNow();
2032  }
2033  // @note: $cachedEdit is safely not used if the rev ID was referenced in the text
2034  $edit->revid = $revid;
2035 
2036  if ( $cachedEdit ) {
2037  $edit->pstContent = $cachedEdit->pstContent;
2038  } else {
2039  $edit->pstContent = $content
2040  ? $content->preSaveTransform( $this->mTitle, $user, $popts )
2041  : null;
2042  }
2043 
2044  $edit->format = $serialFormat;
2045  $edit->popts = $this->makeParserOptions( 'canonical' );
2046  if ( $cachedEdit ) {
2047  $edit->output = $cachedEdit->output;
2048  } else {
2049  if ( $revision ) {
2050  // We get here if vary-revision is set. This means that this page references
2051  // itself (such as via self-transclusion). In this case, we need to make sure
2052  // that any such self-references refer to the newly-saved revision, and not
2053  // to the previous one, which could otherwise happen due to replica DB lag.
2054  $oldCallback = $edit->popts->getCurrentRevisionCallback();
2055  $edit->popts->setCurrentRevisionCallback(
2056  function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
2057  if ( $title->equals( $revision->getTitle() ) ) {
2058  return $revision;
2059  } else {
2060  return call_user_func( $oldCallback, $title, $parser );
2061  }
2062  }
2063  );
2064  } else {
2065  // Try to avoid a second parse if {{REVISIONID}} is used
2066  $dbIndex = ( $this->mDataLoadedFrom & self::READ_LATEST ) === self::READ_LATEST
2067  ? DB_MASTER // use the best possible guess
2068  : DB_REPLICA; // T154554
2069 
2070  $edit->popts->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
2071  return 1 + (int)wfGetDB( $dbIndex )->selectField(
2072  'revision',
2073  'MAX(rev_id)',
2074  [],
2075  __METHOD__
2076  );
2077  } );
2078  }
2079  $edit->output = $edit->pstContent
2080  ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2081  : null;
2082  }
2083 
2084  $edit->newContent = $content;
2085  $edit->oldContent = $this->getContent( Revision::RAW );
2086 
2087  // NOTE: B/C for hooks! don't use these fields!
2088  $edit->newText = $edit->newContent
2089  ? ContentHandler::getContentText( $edit->newContent )
2090  : '';
2091  $edit->oldText = $edit->oldContent
2092  ? ContentHandler::getContentText( $edit->oldContent )
2093  : '';
2094  $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
2095 
2096  if ( $edit->output ) {
2097  $edit->output->setCacheTime( wfTimestampNow() );
2098  }
2099 
2100  // Process cache the result
2101  $this->mPreparedEdit = $edit;
2102 
2103  return $edit;
2104  }
2105 
2127  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2129 
2130  $options += [
2131  'changed' => true,
2132  'created' => false,
2133  'moved' => false,
2134  'restored' => false,
2135  'oldrevision' => null,
2136  'oldcountable' => null
2137  ];
2138  $content = $revision->getContent();
2139 
2140  $logger = LoggerFactory::getInstance( 'SaveParse' );
2141 
2142  // See if the parser output before $revision was inserted is still valid
2143  $editInfo = false;
2144  if ( !$this->mPreparedEdit ) {
2145  $logger->debug( __METHOD__ . ": No prepared edit...\n" );
2146  } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
2147  $logger->info( __METHOD__ . ": Prepared edit has vary-revision...\n" );
2148  } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision-id' )
2149  && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()
2150  ) {
2151  $logger->info( __METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n" );
2152  } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-user' ) && !$options['changed'] ) {
2153  $logger->info( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
2154  } else {
2155  wfDebug( __METHOD__ . ": Using prepared edit...\n" );
2156  $editInfo = $this->mPreparedEdit;
2157  }
2158 
2159  if ( !$editInfo ) {
2160  // Parse the text again if needed. Be careful not to do pre-save transform twice:
2161  // $text is usually already pre-save transformed once. Avoid using the edit stash
2162  // as any prepared content from there or in doEditContent() was already rejected.
2163  $editInfo = $this->prepareContentForEdit( $content, $revision, $user, null, false );
2164  }
2165 
2166  // Save it to the parser cache.
2167  // Make sure the cache time matches page_touched to avoid double parsing.
2168  MediaWikiServices::getInstance()->getParserCache()->save(
2169  $editInfo->output, $this, $editInfo->popts,
2170  $revision->getTimestamp(), $editInfo->revid
2171  );
2172 
2173  // Update the links tables and other secondary data
2174  if ( $content ) {
2175  $recursive = $options['changed']; // T52785
2176  $updates = $content->getSecondaryDataUpdates(
2177  $this->getTitle(), null, $recursive, $editInfo->output
2178  );
2179  foreach ( $updates as $update ) {
2180  if ( $update instanceof LinksUpdate ) {
2181  $update->setRevision( $revision );
2182  $update->setTriggeringUser( $user );
2183  }
2184  DeferredUpdates::addUpdate( $update );
2185  }
2187  && $this->getContentHandler()->supportsCategories() === true
2188  && ( $options['changed'] || $options['created'] )
2189  && !$options['restored']
2190  ) {
2191  // Note: jobs are pushed after deferred updates, so the job should be able to see
2192  // the recent change entry (also done via deferred updates) and carry over any
2193  // bot/deletion/IP flags, ect.
2195  $this->getTitle(),
2196  [
2197  'pageId' => $this->getId(),
2198  'revTimestamp' => $revision->getTimestamp()
2199  ]
2200  ) );
2201  }
2202  }
2203 
2204  // Avoid PHP 7.1 warning of passing $this by reference
2205  $wikiPage = $this;
2206 
2207  Hooks::run( 'ArticleEditUpdates', [ &$wikiPage, &$editInfo, $options['changed'] ] );
2208 
2209  if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
2210  // Flush old entries from the `recentchanges` table
2211  if ( mt_rand( 0, 9 ) == 0 ) {
2213  }
2214  }
2215 
2216  if ( !$this->exists() ) {
2217  return;
2218  }
2219 
2220  $id = $this->getId();
2221  $title = $this->mTitle->getPrefixedDBkey();
2222  $shortTitle = $this->mTitle->getDBkey();
2223 
2224  if ( $options['oldcountable'] === 'no-change' ||
2225  ( !$options['changed'] && !$options['moved'] )
2226  ) {
2227  $good = 0;
2228  } elseif ( $options['created'] ) {
2229  $good = (int)$this->isCountable( $editInfo );
2230  } elseif ( $options['oldcountable'] !== null ) {
2231  $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
2232  } else {
2233  $good = 0;
2234  }
2235  $edits = $options['changed'] ? 1 : 0;
2236  $total = $options['created'] ? 1 : 0;
2237 
2238  DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
2239  DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
2240 
2241  // If this is another user's talk page, update newtalk.
2242  // Don't do this if $options['changed'] = false (null-edits) nor if
2243  // it's a minor edit and the user doesn't want notifications for those.
2244  if ( $options['changed']
2245  && $this->mTitle->getNamespace() == NS_USER_TALK
2246  && $shortTitle != $user->getTitleKey()
2247  && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
2248  ) {
2249  $recipient = User::newFromName( $shortTitle, false );
2250  if ( !$recipient ) {
2251  wfDebug( __METHOD__ . ": invalid username\n" );
2252  } else {
2253  // Avoid PHP 7.1 warning of passing $this by reference
2254  $wikiPage = $this;
2255 
2256  // Allow extensions to prevent user notification
2257  // when a new message is added to their talk page
2258  if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
2259  if ( User::isIP( $shortTitle ) ) {
2260  // An anonymous user
2261  $recipient->setNewtalk( true, $revision );
2262  } elseif ( $recipient->isLoggedIn() ) {
2263  $recipient->setNewtalk( true, $revision );
2264  } else {
2265  wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
2266  }
2267  }
2268  }
2269  }
2270 
2271  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
2272  MessageCache::singleton()->updateMessageOverride( $this->mTitle, $content );
2273  }
2274 
2275  if ( $options['created'] ) {
2276  self::onArticleCreate( $this->mTitle );
2277  } elseif ( $options['changed'] ) { // T52785
2278  self::onArticleEdit( $this->mTitle, $revision );
2279  }
2280 
2282  $this->mTitle, $options['oldrevision'], $revision, wfWikiID()
2283  );
2284  }
2285 
2300  public function doUpdateRestrictions( array $limit, array $expiry,
2301  &$cascade, $reason, User $user, $tags = null
2302  ) {
2304 
2305  if ( wfReadOnly() ) {
2306  return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2307  }
2308 
2309  $this->loadPageData( 'fromdbmaster' );
2310  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2311  $id = $this->getId();
2312 
2313  if ( !$cascade ) {
2314  $cascade = false;
2315  }
2316 
2317  // Take this opportunity to purge out expired restrictions
2319 
2320  // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
2321  // we expect a single selection, but the schema allows otherwise.
2322  $isProtected = false;
2323  $protect = false;
2324  $changed = false;
2325 
2326  $dbw = wfGetDB( DB_MASTER );
2327 
2328  foreach ( $restrictionTypes as $action ) {
2329  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2330  $expiry[$action] = 'infinity';
2331  }
2332  if ( !isset( $limit[$action] ) ) {
2333  $limit[$action] = '';
2334  } elseif ( $limit[$action] != '' ) {
2335  $protect = true;
2336  }
2337 
2338  // Get current restrictions on $action
2339  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2340  if ( $current != '' ) {
2341  $isProtected = true;
2342  }
2343 
2344  if ( $limit[$action] != $current ) {
2345  $changed = true;
2346  } elseif ( $limit[$action] != '' ) {
2347  // Only check expiry change if the action is actually being
2348  // protected, since expiry does nothing on an not-protected
2349  // action.
2350  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2351  $changed = true;
2352  }
2353  }
2354  }
2355 
2356  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2357  $changed = true;
2358  }
2359 
2360  // If nothing has changed, do nothing
2361  if ( !$changed ) {
2362  return Status::newGood();
2363  }
2364 
2365  if ( !$protect ) { // No protection at all means unprotection
2366  $revCommentMsg = 'unprotectedarticle-comment';
2367  $logAction = 'unprotect';
2368  } elseif ( $isProtected ) {
2369  $revCommentMsg = 'modifiedarticleprotection-comment';
2370  $logAction = 'modify';
2371  } else {
2372  $revCommentMsg = 'protectedarticle-comment';
2373  $logAction = 'protect';
2374  }
2375 
2376  $logRelationsValues = [];
2377  $logRelationsField = null;
2378  $logParamsDetails = [];
2379 
2380  // Null revision (used for change tag insertion)
2381  $nullRevision = null;
2382 
2383  if ( $id ) { // Protection of existing page
2384  // Avoid PHP 7.1 warning of passing $this by reference
2385  $wikiPage = $this;
2386 
2387  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2388  return Status::newGood();
2389  }
2390 
2391  // Only certain restrictions can cascade...
2392  $editrestriction = isset( $limit['edit'] )
2393  ? [ $limit['edit'] ]
2394  : $this->mTitle->getRestrictions( 'edit' );
2395  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2396  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2397  }
2398  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2399  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2400  }
2401 
2402  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2403  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2404  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2405  }
2406  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2407  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2408  }
2409 
2410  // The schema allows multiple restrictions
2411  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2412  $cascade = false;
2413  }
2414 
2415  // insert null revision to identify the page protection change as edit summary
2416  $latest = $this->getLatest();
2417  $nullRevision = $this->insertProtectNullRevision(
2418  $revCommentMsg,
2419  $limit,
2420  $expiry,
2421  $cascade,
2422  $reason,
2423  $user
2424  );
2425 
2426  if ( $nullRevision === null ) {
2427  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2428  }
2429 
2430  $logRelationsField = 'pr_id';
2431 
2432  // Update restrictions table
2433  foreach ( $limit as $action => $restrictions ) {
2434  $dbw->delete(
2435  'page_restrictions',
2436  [
2437  'pr_page' => $id,
2438  'pr_type' => $action
2439  ],
2440  __METHOD__
2441  );
2442  if ( $restrictions != '' ) {
2443  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2444  $dbw->insert(
2445  'page_restrictions',
2446  [
2447  'pr_page' => $id,
2448  'pr_type' => $action,
2449  'pr_level' => $restrictions,
2450  'pr_cascade' => $cascadeValue,
2451  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2452  ],
2453  __METHOD__
2454  );
2455  $logRelationsValues[] = $dbw->insertId();
2456  $logParamsDetails[] = [
2457  'type' => $action,
2458  'level' => $restrictions,
2459  'expiry' => $expiry[$action],
2460  'cascade' => (bool)$cascadeValue,
2461  ];
2462  }
2463  }
2464 
2465  // Clear out legacy restriction fields
2466  $dbw->update(
2467  'page',
2468  [ 'page_restrictions' => '' ],
2469  [ 'page_id' => $id ],
2470  __METHOD__
2471  );
2472 
2473  // Avoid PHP 7.1 warning of passing $this by reference
2474  $wikiPage = $this;
2475 
2476  Hooks::run( 'NewRevisionFromEditComplete',
2477  [ $this, $nullRevision, $latest, $user ] );
2478  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2479  } else { // Protection of non-existing page (also known as "title protection")
2480  // Cascade protection is meaningless in this case
2481  $cascade = false;
2482 
2483  if ( $limit['create'] != '' ) {
2484  $commentFields = CommentStore::newKey( 'pt_reason' )->insert( $dbw, $reason );
2485  $dbw->replace( 'protected_titles',
2486  [ [ 'pt_namespace', 'pt_title' ] ],
2487  [
2488  'pt_namespace' => $this->mTitle->getNamespace(),
2489  'pt_title' => $this->mTitle->getDBkey(),
2490  'pt_create_perm' => $limit['create'],
2491  'pt_timestamp' => $dbw->timestamp(),
2492  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2493  'pt_user' => $user->getId(),
2494  ] + $commentFields, __METHOD__
2495  );
2496  $logParamsDetails[] = [
2497  'type' => 'create',
2498  'level' => $limit['create'],
2499  'expiry' => $expiry['create'],
2500  ];
2501  } else {
2502  $dbw->delete( 'protected_titles',
2503  [
2504  'pt_namespace' => $this->mTitle->getNamespace(),
2505  'pt_title' => $this->mTitle->getDBkey()
2506  ], __METHOD__
2507  );
2508  }
2509  }
2510 
2511  $this->mTitle->flushRestrictions();
2512  InfoAction::invalidateCache( $this->mTitle );
2513 
2514  if ( $logAction == 'unprotect' ) {
2515  $params = [];
2516  } else {
2517  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2518  $params = [
2519  '4::description' => $protectDescriptionLog, // parameter for IRC
2520  '5:bool:cascade' => $cascade,
2521  'details' => $logParamsDetails, // parameter for localize and api
2522  ];
2523  }
2524 
2525  // Update the protection log
2526  $logEntry = new ManualLogEntry( 'protect', $logAction );
2527  $logEntry->setTarget( $this->mTitle );
2528  $logEntry->setComment( $reason );
2529  $logEntry->setPerformer( $user );
2530  $logEntry->setParameters( $params );
2531  if ( !is_null( $nullRevision ) ) {
2532  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2533  }
2534  $logEntry->setTags( $tags );
2535  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2536  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2537  }
2538  $logId = $logEntry->insert();
2539  $logEntry->publish( $logId );
2540 
2541  return Status::newGood( $logId );
2542  }
2543 
2555  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2556  array $expiry, $cascade, $reason, $user = null
2557  ) {
2558  $dbw = wfGetDB( DB_MASTER );
2559 
2560  // Prepare a null revision to be added to the history
2561  $editComment = wfMessage(
2562  $revCommentMsg,
2563  $this->mTitle->getPrefixedText(),
2564  $user ? $user->getName() : ''
2565  )->inContentLanguage()->text();
2566  if ( $reason ) {
2567  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2568  }
2569  $protectDescription = $this->protectDescription( $limit, $expiry );
2570  if ( $protectDescription ) {
2571  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2572  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2573  ->inContentLanguage()->text();
2574  }
2575  if ( $cascade ) {
2576  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2577  $editComment .= wfMessage( 'brackets' )->params(
2578  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2579  )->inContentLanguage()->text();
2580  }
2581 
2582  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2583  if ( $nullRev ) {
2584  $nullRev->insertOn( $dbw );
2585 
2586  // Update page record and touch page
2587  $oldLatest = $nullRev->getParentId();
2588  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2589  }
2590 
2591  return $nullRev;
2592  }
2593 
2598  protected function formatExpiry( $expiry ) {
2600 
2601  if ( $expiry != 'infinity' ) {
2602  return wfMessage(
2603  'protect-expiring',
2604  $wgContLang->timeanddate( $expiry, false, false ),
2605  $wgContLang->date( $expiry, false, false ),
2606  $wgContLang->time( $expiry, false, false )
2607  )->inContentLanguage()->text();
2608  } else {
2609  return wfMessage( 'protect-expiry-indefinite' )
2610  ->inContentLanguage()->text();
2611  }
2612  }
2613 
2621  public function protectDescription( array $limit, array $expiry ) {
2622  $protectDescription = '';
2623 
2624  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2625  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2626  # All possible message keys are listed here for easier grepping:
2627  # * restriction-create
2628  # * restriction-edit
2629  # * restriction-move
2630  # * restriction-upload
2631  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2632  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2633  # with '' filtered out. All possible message keys are listed below:
2634  # * protect-level-autoconfirmed
2635  # * protect-level-sysop
2636  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2637  ->inContentLanguage()->text();
2638 
2639  $expiryText = $this->formatExpiry( $expiry[$action] );
2640 
2641  if ( $protectDescription !== '' ) {
2642  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2643  }
2644  $protectDescription .= wfMessage( 'protect-summary-desc' )
2645  ->params( $actionText, $restrictionsText, $expiryText )
2646  ->inContentLanguage()->text();
2647  }
2648 
2649  return $protectDescription;
2650  }
2651 
2663  public function protectDescriptionLog( array $limit, array $expiry ) {
2665 
2666  $protectDescriptionLog = '';
2667 
2668  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2669  $expiryText = $this->formatExpiry( $expiry[$action] );
2670  $protectDescriptionLog .= $wgContLang->getDirMark() .
2671  "[$action=$restrictions] ($expiryText)";
2672  }
2673 
2674  return trim( $protectDescriptionLog );
2675  }
2676 
2686  protected static function flattenRestrictions( $limit ) {
2687  if ( !is_array( $limit ) ) {
2688  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2689  }
2690 
2691  $bits = [];
2692  ksort( $limit );
2693 
2694  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2695  $bits[] = "$action=$restrictions";
2696  }
2697 
2698  return implode( ':', $bits );
2699  }
2700 
2717  public function doDeleteArticle(
2718  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2719  ) {
2720  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
2721  return $status->isGood();
2722  }
2723 
2743  public function doDeleteArticleReal(
2744  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2745  $tags = [], $logsubtype = 'delete'
2746  ) {
2748 
2749  wfDebug( __METHOD__ . "\n" );
2750 
2752 
2753  if ( $this->mTitle->getDBkey() === '' ) {
2754  $status->error( 'cannotdelete',
2755  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2756  return $status;
2757  }
2758 
2759  // Avoid PHP 7.1 warning of passing $this by reference
2760  $wikiPage = $this;
2761 
2762  $user = is_null( $user ) ? $wgUser : $user;
2763  if ( !Hooks::run( 'ArticleDelete',
2764  [ &$wikiPage, &$user, &$reason, &$error, &$status, $suppress ]
2765  ) ) {
2766  if ( $status->isOK() ) {
2767  // Hook aborted but didn't set a fatal status
2768  $status->fatal( 'delete-hook-aborted' );
2769  }
2770  return $status;
2771  }
2772 
2773  $dbw = wfGetDB( DB_MASTER );
2774  $dbw->startAtomic( __METHOD__ );
2775 
2776  $this->loadPageData( self::READ_LATEST );
2777  $id = $this->getId();
2778  // T98706: lock the page from various other updates but avoid using
2779  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2780  // the revisions queries (which also JOIN on user). Only lock the page
2781  // row and CAS check on page_latest to see if the trx snapshot matches.
2782  $lockedLatest = $this->lockAndGetLatest();
2783  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2784  $dbw->endAtomic( __METHOD__ );
2785  // Page not there or trx snapshot is stale
2786  $status->error( 'cannotdelete',
2787  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2788  return $status;
2789  }
2790 
2791  // Given the lock above, we can be confident in the title and page ID values
2792  $namespace = $this->getTitle()->getNamespace();
2793  $dbKey = $this->getTitle()->getDBkey();
2794 
2795  // At this point we are now comitted to returning an OK
2796  // status unless some DB query error or other exception comes up.
2797  // This way callers don't have to call rollback() if $status is bad
2798  // unless they actually try to catch exceptions (which is rare).
2799 
2800  // we need to remember the old content so we can use it to generate all deletion updates.
2801  $revision = $this->getRevision();
2802  try {
2803  $content = $this->getContent( Revision::RAW );
2804  } catch ( Exception $ex ) {
2805  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2806  . $ex->getMessage() );
2807 
2808  $content = null;
2809  }
2810 
2811  $revCommentStore = new CommentStore( 'rev_comment' );
2812  $arCommentStore = new CommentStore( 'ar_comment' );
2813 
2814  $fields = Revision::selectFields();
2815  $bitfield = false;
2816 
2817  // Bitfields to further suppress the content
2818  if ( $suppress ) {
2819  $bitfield = Revision::SUPPRESSED_ALL;
2820  $fields = array_diff( $fields, [ 'rev_deleted' ] );
2821  }
2822 
2823  // For now, shunt the revision data into the archive table.
2824  // Text is *not* removed from the text table; bulk storage
2825  // is left intact to avoid breaking block-compression or
2826  // immutable storage schemes.
2827  // In the future, we may keep revisions and mark them with
2828  // the rev_deleted field, which is reserved for this purpose.
2829 
2830  // Get all of the page revisions
2831  $commentQuery = $revCommentStore->getJoin();
2832  $res = $dbw->select(
2833  [ 'revision' ] + $commentQuery['tables'],
2834  $fields + $commentQuery['fields'],
2835  [ 'rev_page' => $id ],
2836  __METHOD__,
2837  'FOR UPDATE',
2838  $commentQuery['joins']
2839  );
2840 
2841  // Build their equivalent archive rows
2842  $rowsInsert = [];
2843  $revids = [];
2844 
2846  $ipRevIds = [];
2847 
2848  foreach ( $res as $row ) {
2849  $comment = $revCommentStore->getComment( $row );
2850  $rowInsert = [
2851  'ar_namespace' => $namespace,
2852  'ar_title' => $dbKey,
2853  'ar_user' => $row->rev_user,
2854  'ar_user_text' => $row->rev_user_text,
2855  'ar_timestamp' => $row->rev_timestamp,
2856  'ar_minor_edit' => $row->rev_minor_edit,
2857  'ar_rev_id' => $row->rev_id,
2858  'ar_parent_id' => $row->rev_parent_id,
2859  'ar_text_id' => $row->rev_text_id,
2860  'ar_text' => '',
2861  'ar_flags' => '',
2862  'ar_len' => $row->rev_len,
2863  'ar_page_id' => $id,
2864  'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2865  'ar_sha1' => $row->rev_sha1,
2866  ] + $arCommentStore->insert( $dbw, $comment );
2867  if ( $wgContentHandlerUseDB ) {
2868  $rowInsert['ar_content_model'] = $row->rev_content_model;
2869  $rowInsert['ar_content_format'] = $row->rev_content_format;
2870  }
2871  $rowsInsert[] = $rowInsert;
2872  $revids[] = $row->rev_id;
2873 
2874  // Keep track of IP edits, so that the corresponding rows can
2875  // be deleted in the ip_changes table.
2876  if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2877  $ipRevIds[] = $row->rev_id;
2878  }
2879  }
2880  // Copy them into the archive table
2881  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2882  // Save this so we can pass it to the ArticleDeleteComplete hook.
2883  $archivedRevisionCount = $dbw->affectedRows();
2884 
2885  // Clone the title and wikiPage, so we have the information we need when
2886  // we log and run the ArticleDeleteComplete hook.
2887  $logTitle = clone $this->mTitle;
2888  $wikiPageBeforeDelete = clone $this;
2889 
2890  // Now that it's safely backed up, delete it
2891  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2892  $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
2894  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2895  }
2896 
2897  // Also delete records from ip_changes as applicable.
2898  if ( count( $ipRevIds ) > 0 ) {
2899  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2900  }
2901 
2902  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2903  $logtype = $suppress ? 'suppress' : 'delete';
2904 
2905  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2906  $logEntry->setPerformer( $user );
2907  $logEntry->setTarget( $logTitle );
2908  $logEntry->setComment( $reason );
2909  $logEntry->setTags( $tags );
2910  $logid = $logEntry->insert();
2911 
2912  $dbw->onTransactionPreCommitOrIdle(
2913  function () use ( $dbw, $logEntry, $logid ) {
2914  // T58776: avoid deadlocks (especially from FileDeleteForm)
2915  $logEntry->publish( $logid );
2916  },
2917  __METHOD__
2918  );
2919 
2920  $dbw->endAtomic( __METHOD__ );
2921 
2922  $this->doDeleteUpdates( $id, $content, $revision );
2923 
2924  Hooks::run( 'ArticleDeleteComplete', [
2925  &$wikiPageBeforeDelete,
2926  &$user,
2927  $reason,
2928  $id,
2929  $content,
2930  $logEntry,
2931  $archivedRevisionCount
2932  ] );
2933  $status->value = $logid;
2934 
2935  // Show log excerpt on 404 pages rather than just a link
2936  $cache = MediaWikiServices::getInstance()->getMainObjectStash();
2937  $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2938  $cache->set( $key, 1, $cache::TTL_DAY );
2939 
2940  return $status;
2941  }
2942 
2949  public function lockAndGetLatest() {
2950  return (int)wfGetDB( DB_MASTER )->selectField(
2951  'page',
2952  'page_latest',
2953  [
2954  'page_id' => $this->getId(),
2955  // Typically page_id is enough, but some code might try to do
2956  // updates assuming the title is the same, so verify that
2957  'page_namespace' => $this->getTitle()->getNamespace(),
2958  'page_title' => $this->getTitle()->getDBkey()
2959  ],
2960  __METHOD__,
2961  [ 'FOR UPDATE' ]
2962  );
2963  }
2964 
2974  public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null ) {
2975  try {
2976  $countable = $this->isCountable();
2977  } catch ( Exception $ex ) {
2978  // fallback for deleting broken pages for which we cannot load the content for
2979  // some reason. Note that doDeleteArticleReal() already logged this problem.
2980  $countable = false;
2981  }
2982 
2983  // Update site status
2984  DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
2985 
2986  // Delete pagelinks, update secondary indexes, etc
2987  $updates = $this->getDeletionUpdates( $content );
2988  foreach ( $updates as $update ) {
2989  DeferredUpdates::addUpdate( $update );
2990  }
2991 
2992  // Reparse any pages transcluding this page
2993  LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
2994 
2995  // Reparse any pages including this image
2996  if ( $this->mTitle->getNamespace() == NS_FILE ) {
2997  LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
2998  }
2999 
3000  // Clear caches
3001  self::onArticleDelete( $this->mTitle );
3003  $this->mTitle, $revision, null, wfWikiID()
3004  );
3005 
3006  // Reset this object and the Title object
3007  $this->loadFromRow( false, self::READ_LATEST );
3008 
3009  // Search engine
3010  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3011  }
3012 
3042  public function doRollback(
3043  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3044  ) {
3045  $resultDetails = null;
3046 
3047  // Check permissions
3048  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3049  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3050  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3051 
3052  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3053  $errors[] = [ 'sessionfailure' ];
3054  }
3055 
3056  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3057  $errors[] = [ 'actionthrottledtext' ];
3058  }
3059 
3060  // If there were errors, bail out now
3061  if ( !empty( $errors ) ) {
3062  return $errors;
3063  }
3064 
3065  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3066  }
3067 
3088  public function commitRollback( $fromP, $summary, $bot,
3089  &$resultDetails, User $guser, $tags = null
3090  ) {
3092 
3093  $dbw = wfGetDB( DB_MASTER );
3094 
3095  if ( wfReadOnly() ) {
3096  return [ [ 'readonlytext' ] ];
3097  }
3098 
3099  // Get the last editor
3100  $current = $this->getRevision();
3101  if ( is_null( $current ) ) {
3102  // Something wrong... no page?
3103  return [ [ 'notanarticle' ] ];
3104  }
3105 
3106  $from = str_replace( '_', ' ', $fromP );
3107  // User name given should match up with the top revision.
3108  // If the user was deleted then $from should be empty.
3109  if ( $from != $current->getUserText() ) {
3110  $resultDetails = [ 'current' => $current ];
3111  return [ [ 'alreadyrolled',
3112  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3113  htmlspecialchars( $fromP ),
3114  htmlspecialchars( $current->getUserText() )
3115  ] ];
3116  }
3117 
3118  // Get the last edit not by this person...
3119  // Note: these may not be public values
3120  $user = intval( $current->getUser( Revision::RAW ) );
3121  $user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
3122  $s = $dbw->selectRow( 'revision',
3123  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3124  [ 'rev_page' => $current->getPage(),
3125  "rev_user != {$user} OR rev_user_text != {$user_text}"
3126  ], __METHOD__,
3127  [ 'USE INDEX' => 'page_timestamp',
3128  'ORDER BY' => 'rev_timestamp DESC' ]
3129  );
3130  if ( $s === false ) {
3131  // No one else ever edited this page
3132  return [ [ 'cantrollback' ] ];
3133  } elseif ( $s->rev_deleted & Revision::DELETED_TEXT
3134  || $s->rev_deleted & Revision::DELETED_USER
3135  ) {
3136  // Only admins can see this text
3137  return [ [ 'notvisiblerev' ] ];
3138  }
3139 
3140  // Generate the edit summary if necessary
3141  $target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST );
3142  if ( empty( $summary ) ) {
3143  if ( $from == '' ) { // no public user name
3144  $summary = wfMessage( 'revertpage-nouser' );
3145  } else {
3146  $summary = wfMessage( 'revertpage' );
3147  }
3148  }
3149 
3150  // Allow the custom summary to use the same args as the default message
3151  $args = [
3152  $target->getUserText(), $from, $s->rev_id,
3153  $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3154  $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
3155  ];
3156  if ( $summary instanceof Message ) {
3157  $summary = $summary->params( $args )->inContentLanguage()->text();
3158  } else {
3159  $summary = wfMsgReplaceArgs( $summary, $args );
3160  }
3161 
3162  // Trim spaces on user supplied text
3163  $summary = trim( $summary );
3164 
3165  // Save
3167 
3168  if ( $guser->isAllowed( 'minoredit' ) ) {
3169  $flags |= EDIT_MINOR;
3170  }
3171 
3172  if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3174  }
3175 
3176  $targetContent = $target->getContent();
3177  $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
3178 
3179  // Actually store the edit
3180  $status = $this->doEditContent(
3181  $targetContent,
3182  $summary,
3183  $flags,
3184  $target->getId(),
3185  $guser,
3186  null,
3187  $tags
3188  );
3189 
3190  // Set patrolling and bot flag on the edits, which gets rollbacked.
3191  // This is done even on edit failure to have patrolling in that case (T64157).
3192  $set = [];
3193  if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3194  // Mark all reverted edits as bot
3195  $set['rc_bot'] = 1;
3196  }
3197 
3198  if ( $wgUseRCPatrol ) {
3199  // Mark all reverted edits as patrolled
3200  $set['rc_patrolled'] = 1;
3201  }
3202 
3203  if ( count( $set ) ) {
3204  $dbw->update( 'recentchanges', $set,
3205  [ /* WHERE */
3206  'rc_cur_id' => $current->getPage(),
3207  'rc_user_text' => $current->getUserText(),
3208  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3209  ],
3210  __METHOD__
3211  );
3212  }
3213 
3214  if ( !$status->isOK() ) {
3215  return $status->getErrorsArray();
3216  }
3217 
3218  // raise error, when the edit is an edit without a new version
3219  $statusRev = isset( $status->value['revision'] )
3220  ? $status->value['revision']
3221  : null;
3222  if ( !( $statusRev instanceof Revision ) ) {
3223  $resultDetails = [ 'current' => $current ];
3224  return [ [ 'alreadyrolled',
3225  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3226  htmlspecialchars( $fromP ),
3227  htmlspecialchars( $current->getUserText() )
3228  ] ];
3229  }
3230 
3231  if ( $changingContentModel ) {
3232  // If the content model changed during the rollback,
3233  // make sure it gets logged to Special:Log/contentmodel
3234  $log = new ManualLogEntry( 'contentmodel', 'change' );
3235  $log->setPerformer( $guser );
3236  $log->setTarget( $this->mTitle );
3237  $log->setComment( $summary );
3238  $log->setParameters( [
3239  '4::oldmodel' => $current->getContentModel(),
3240  '5::newmodel' => $targetContent->getModel(),
3241  ] );
3242 
3243  $logId = $log->insert( $dbw );
3244  $log->publish( $logId );
3245  }
3246 
3247  $revId = $statusRev->getId();
3248 
3249  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3250 
3251  $resultDetails = [
3252  'summary' => $summary,
3253  'current' => $current,
3254  'target' => $target,
3255  'newid' => $revId
3256  ];
3257 
3258  return [];
3259  }
3260 
3272  public static function onArticleCreate( Title $title ) {
3273  // Update existence markers on article/talk tabs...
3274  $other = $title->getOtherPage();
3275 
3276  $other->purgeSquid();
3277 
3278  $title->touchLinks();
3279  $title->purgeSquid();
3280  $title->deleteTitleProtection();
3281 
3282  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3283 
3284  // Invalidate caches of articles which include this page
3285  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
3286 
3287  if ( $title->getNamespace() == NS_CATEGORY ) {
3288  // Load the Category object, which will schedule a job to create
3289  // the category table row if necessary. Checking a replica DB is ok
3290  // here, in the worst case it'll run an unnecessary recount job on
3291  // a category that probably doesn't have many members.
3292  Category::newFromTitle( $title )->getID();
3293  }
3294  }
3295 
3301  public static function onArticleDelete( Title $title ) {
3302  // Update existence markers on article/talk tabs...
3303  $other = $title->getOtherPage();
3304 
3305  $other->purgeSquid();
3306 
3307  $title->touchLinks();
3308  $title->purgeSquid();
3309 
3310  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3311 
3312  // File cache
3315 
3316  // Messages
3317  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3318  MessageCache::singleton()->updateMessageOverride( $title, null );
3319  }
3320 
3321  // Images
3322  if ( $title->getNamespace() == NS_FILE ) {
3323  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
3324  }
3325 
3326  // User talk pages
3327  if ( $title->getNamespace() == NS_USER_TALK ) {
3328  $user = User::newFromName( $title->getText(), false );
3329  if ( $user ) {
3330  $user->setNewtalk( false );
3331  }
3332  }
3333 
3334  // Image redirects
3335  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3336  }
3337 
3344  public static function onArticleEdit( Title $title, Revision $revision = null ) {
3345  // Invalidate caches of articles which include this page
3346  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
3347 
3348  // Invalidate the caches of all pages which redirect here
3349  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
3350 
3351  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3352 
3353  // Purge CDN for this page only
3354  $title->purgeSquid();
3355  // Clear file cache for this page only
3357 
3358  $revid = $revision ? $revision->getId() : null;
3359  DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3361  } );
3362  }
3363 
3372  public function getCategories() {
3373  $id = $this->getId();
3374  if ( $id == 0 ) {
3375  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3376  }
3377 
3378  $dbr = wfGetDB( DB_REPLICA );
3379  $res = $dbr->select( 'categorylinks',
3380  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3381  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3382  // as not being aliases, and NS_CATEGORY is numeric
3383  [ 'cl_from' => $id ],
3384  __METHOD__ );
3385 
3386  return TitleArray::newFromResult( $res );
3387  }
3388 
3395  public function getHiddenCategories() {
3396  $result = [];
3397  $id = $this->getId();
3398 
3399  if ( $id == 0 ) {
3400  return [];
3401  }
3402 
3403  $dbr = wfGetDB( DB_REPLICA );
3404  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3405  [ 'cl_to' ],
3406  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3407  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3408  __METHOD__ );
3409 
3410  if ( $res !== false ) {
3411  foreach ( $res as $row ) {
3412  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3413  }
3414  }
3415 
3416  return $result;
3417  }
3418 
3426  public function getAutoDeleteReason( &$hasHistory ) {
3427  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3428  }
3429 
3440  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3441  $id = $id ?: $this->getId();
3442  $ns = $this->getTitle()->getNamespace();
3443 
3444  $addFields = [ 'cat_pages = cat_pages + 1' ];
3445  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3446  if ( $ns == NS_CATEGORY ) {
3447  $addFields[] = 'cat_subcats = cat_subcats + 1';
3448  $removeFields[] = 'cat_subcats = cat_subcats - 1';
3449  } elseif ( $ns == NS_FILE ) {
3450  $addFields[] = 'cat_files = cat_files + 1';
3451  $removeFields[] = 'cat_files = cat_files - 1';
3452  }
3453 
3454  $dbw = wfGetDB( DB_MASTER );
3455 
3456  if ( count( $added ) ) {
3457  $existingAdded = $dbw->selectFieldValues(
3458  'category',
3459  'cat_title',
3460  [ 'cat_title' => $added ],
3461  __METHOD__
3462  );
3463 
3464  // For category rows that already exist, do a plain
3465  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3466  // to avoid creating gaps in the cat_id sequence.
3467  if ( count( $existingAdded ) ) {
3468  $dbw->update(
3469  'category',
3470  $addFields,
3471  [ 'cat_title' => $existingAdded ],
3472  __METHOD__
3473  );
3474  }
3475 
3476  $missingAdded = array_diff( $added, $existingAdded );
3477  if ( count( $missingAdded ) ) {
3478  $insertRows = [];
3479  foreach ( $missingAdded as $cat ) {
3480  $insertRows[] = [
3481  'cat_title' => $cat,
3482  'cat_pages' => 1,
3483  'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
3484  'cat_files' => ( $ns == NS_FILE ) ? 1 : 0,
3485  ];
3486  }
3487  $dbw->upsert(
3488  'category',
3489  $insertRows,
3490  [ 'cat_title' ],
3491  $addFields,
3492  __METHOD__
3493  );
3494  }
3495  }
3496 
3497  if ( count( $deleted ) ) {
3498  $dbw->update(
3499  'category',
3500  $removeFields,
3501  [ 'cat_title' => $deleted ],
3502  __METHOD__
3503  );
3504  }
3505 
3506  foreach ( $added as $catName ) {
3507  $cat = Category::newFromName( $catName );
3508  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3509  }
3510 
3511  foreach ( $deleted as $catName ) {
3512  $cat = Category::newFromName( $catName );
3513  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3514  }
3515 
3516  // Refresh counts on categories that should be empty now, to
3517  // trigger possible deletion. Check master for the most
3518  // up-to-date cat_pages.
3519  if ( count( $deleted ) ) {
3520  $rows = $dbw->select(
3521  'category',
3522  [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
3523  [ 'cat_title' => $deleted, 'cat_pages <= 0' ],
3524  __METHOD__
3525  );
3526  foreach ( $rows as $row ) {
3527  $cat = Category::newFromRow( $row );
3528  // T166757: do the update after this DB commit
3529  DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3530  $cat->refreshCounts();
3531  } );
3532  }
3533  }
3534  }
3535 
3542  public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3543  if ( wfReadOnly() ) {
3544  return;
3545  }
3546 
3547  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3548  [ $this, $this->mTitle, $parserOutput ]
3549  ) ) {
3550  return;
3551  }
3552 
3553  $config = RequestContext::getMain()->getConfig();
3554 
3555  $params = [
3556  'isOpportunistic' => true,
3557  'rootJobTimestamp' => $parserOutput->getCacheTime()
3558  ];
3559 
3560  if ( $this->mTitle->areRestrictionsCascading() ) {
3561  // If the page is cascade protecting, the links should really be up-to-date
3562  JobQueueGroup::singleton()->lazyPush(
3563  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3564  );
3565  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3566  // Assume the output contains "dynamic" time/random based magic words.
3567  // Only update pages that expired due to dynamic content and NOT due to edits
3568  // to referenced templates/files. When the cache expires due to dynamic content,
3569  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3570  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3571  // template/file edit already triggered recursive RefreshLinksJob jobs.
3572  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3573  // If a page is uncacheable, do not keep spamming a job for it.
3574  // Although it would be de-duplicated, it would still waste I/O.
3576  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3577  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3578  if ( $cache->add( $key, time(), $ttl ) ) {
3579  JobQueueGroup::singleton()->lazyPush(
3580  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3581  );
3582  }
3583  }
3584  }
3585  }
3586 
3596  public function getDeletionUpdates( Content $content = null ) {
3597  if ( !$content ) {
3598  // load content object, which may be used to determine the necessary updates.
3599  // XXX: the content may not be needed to determine the updates.
3600  try {
3601  $content = $this->getContent( Revision::RAW );
3602  } catch ( Exception $ex ) {
3603  // If we can't load the content, something is wrong. Perhaps that's why
3604  // the user is trying to delete the page, so let's not fail in that case.
3605  // Note that doDeleteArticleReal() will already have logged an issue with
3606  // loading the content.
3607  }
3608  }
3609 
3610  if ( !$content ) {
3611  $updates = [];
3612  } else {
3613  $updates = $content->getDeletionUpdates( $this );
3614  }
3615 
3616  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3617  return $updates;
3618  }
3619 
3627  public function isLocal() {
3628  return true;
3629  }
3630 
3640  public function getWikiDisplayName() {
3642  return $wgSitename;
3643  }
3644 
3653  public function getSourceURL() {
3654  return $this->getTitle()->getCanonicalURL();
3655  }
3656 
3663  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3664 
3665  return $linkCache->getMutableCacheKeys( $cache, $this->getTitle()->getTitleValue() );
3666  }
3667 }
WikiPage\getCategories
getCategories()
#-
Definition: WikiPage.php:3372
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:98
Revision\DELETED_USER
const DELETED_USER
Definition: Revision.php:92
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:40
WikiPage\doDeleteUpdates
doDeleteUpdates( $id, Content $content=null, Revision $revision=null)
Do some database updates after deletion.
Definition: WikiPage.php:2974
$wgUseAutomaticEditSummaries
$wgUseAutomaticEditSummaries
If user doesn't specify any edit summary when making a an edit, MediaWiki will try to automatically c...
Definition: DefaultSettings.php:6590
Revision\getTimestamp
getTimestamp()
Definition: Revision.php:1186
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:658
$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
CacheTime\getCacheExpiry
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:110
$wgUser
$wgUser
Definition: Setup.php:809
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:3272
WikiPage\doDeleteArticleReal
doDeleteArticleReal( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $user=null, $tags=[], $logsubtype='delete')
Back-end article deletion Deletes the article with database consistency, writes logs,...
Definition: WikiPage.php:2743
WikiPage\loadPageData
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:377
RepoGroup\singleton
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:59
Revision\SUPPRESSED_ALL
const SUPPRESSED_ALL
Definition: Revision.php:95
WikiPage\getAutoDeleteReason
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3426
ParserOutput
Definition: ParserOutput.php:24
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:835
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:357
$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:988
Revision\newFromId
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:116
WikiPage\clearCacheFields
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:258
Title\getFragment
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1444
$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:2581
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:1354
WikiPage\updateRevisionOn
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1212
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:704
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:157
EDIT_INTERNAL
const EDIT_INTERNAL
Definition: Defines.php:160
WikiPage\PURGE_CDN_CACHE
const PURGE_CDN_CACHE
Definition: WikiPage.php:92
captcha-old.count
count
Definition: captcha-old.py:249
wfGetLB
wfGetLB( $wiki=false)
Get a load balancer object.
Definition: GlobalFunctions.php:2869
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:2717
$wgCommentTableSchemaMigrationStage
int $wgCommentTableSchemaMigrationStage
Comment table schema migration stage.
Definition: DefaultSettings.php:8765
WikiPage\hasViewableContent
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition: WikiPage.php:483
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:550
Revision\getContent
getContent( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision content if it's available to the specified audience.
Definition: Revision.php:1065
$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 '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:1963
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:2040
LinksUpdate\queueRecursiveJobsForTable
static queueRecursiveJobsForTable(Title $title, $table)
Queue a RefreshLinks job for any table.
Definition: LinksUpdate.php:334
WikiPage\doViewUpdates
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1096
WikiPage\replaceSectionAtRev
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
Definition: WikiPage.php:1425
WikiPage\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1467
$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). '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:1245
Revision\newKnownCurrent
static newKnownCurrent(IDatabase $db, $pageId, $revId)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: Revision.php:1940
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:1387
WikiPage\getUserText
getUserText( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:742
PoolWorkArticleView
Definition: PoolWorkArticleView.php:22
NS_FILE
const NS_FILE
Definition: Defines.php:71
CommentStore
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
Definition: CommentStore.php:30
$params
$params
Definition: styleTest.css.php:40
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1951
WikiPage\getRedirectURL
getRedirectURL( $rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:943
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1324
wfMsgReplaceArgs
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
Definition: GlobalFunctions.php:1455
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:550
WikiPage\getRevision
getRevision()
Get the latest revision.
Definition: WikiPage.php:645
RefreshLinksJob\newDynamic
static newDynamic(Title $title, array $params)
Definition: RefreshLinksJob.php:75
RefreshLinksJob\newPrioritized
static newPrioritized(Title $title, array $params)
Definition: RefreshLinksJob.php:63
Revision\getContentHandler
getContentHandler()
Returns the content handler appropriate for this revision's content model.
Definition: Revision.php:1167
$s
$s
Definition: mergeMessageFileList.php:188
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:981
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1203
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:582
$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:760
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:165
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
Revision\getId
getId()
Get revision ID.
Definition: Revision.php:743
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:8475
CommentStore\newKey
static newKey( $key)
Static constructor for easier chaining.
Definition: CommentStore.php:114
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:1406
WikiPage\onArticleEdit
static onArticleEdit(Title $title, Revision $revision=null)
Purge caches on page update etc.
Definition: WikiPage.php:3344
Content\getSize
getSize()
Returns the content's nominal size in "bogo-bytes".
Wikimedia\Rdbms\DBError
Database error base class.
Definition: DBError.php:30
WikiPage\$mLatest
$mLatest
Definition: WikiPage.php:50
WikiPage\getActionOverrides
getActionOverrides()
Definition: WikiPage.php:218
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:6801
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:6833
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:2686
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:40
WikiPage\triggerOpportunisticLinksUpdate
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3542
WikiPage\protectDescription
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2621
IDBAccessObject\READ_LOCKING
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:62
Revision
Definition: Revision.php:33
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, IDatabase $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:111
WikiPage\supportsSections
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1369
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:1537
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:955
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:932
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:121
WikiPage\getMinorEdit
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:774
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Definition: GlobalFunctions.php:1176
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:3042
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:978
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:180
Title\newFromRow
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:459
wfIncrStats
wfIncrStats( $key, $count=1)
Increment a statistics counter.
Definition: GlobalFunctions.php:1314
WikiPage\selectFields
static selectFields()
Return the list of revision fields that should be selected to create a new page.
Definition: WikiPage.php:288
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2856
WikiPage\clearPreparedEdit
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:278
Title\getInterwiki
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:865
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1055
WikiPage\getId
getId()
Definition: WikiPage.php:458
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1169
WikiPage\shouldCheckParserCache
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1035
UserArrayFromResult
Definition: UserArrayFromResult.php:25
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:239
wfTimestampOrNull
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Definition: GlobalFunctions.php:2056
WikiPage\getContent
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:666
WikiPage\exists
exists()
Definition: WikiPage.php:468
WikiPage\__clone
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:109
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3301
User\isIP
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:825
WikiPage\$mRedirectTarget
Title $mRedirectTarget
Definition: WikiPage.php:69
WikiPage\__construct
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:101
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:539
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:61
WikiPage\getLinksTimestamp
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:561
$parser
do that in ParserLimitReportFormat instead $parser
Definition: hooks.txt:2572
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:529
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:2069
WikiPage\setTimestamp
setTimestamp( $ts)
Set the page timestamp (use only to avoid DB queries)
Definition: WikiPage.php:691
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:79
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:572
ParserOptions\getStubThreshold
getStubThreshold()
Thumb size preferred by the user.
Definition: ParserOptions.php:560
SiteStatsUpdate
Class for handling updates to the site_stats table.
Definition: SiteStatsUpdate.php:27
WikiPage\PURGE_ALL
const PURGE_ALL
Definition: WikiPage.php:95
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:1047
WikiPage\doPurge
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1117
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:791
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:510
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:347
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2949
CategoryMembershipChangeJob
Job to add recent change entries mentioning category membership changes.
Definition: CategoryMembershipChangeJob.php:36
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:8558
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:1318
WikiPage\PURGE_GLOBAL_PCACHE
const PURGE_GLOBAL_PCACHE
Definition: WikiPage.php:94
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:636
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:2807
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:68
$e
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2141
WikiPage\protectDescriptionLog
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2663
WikiPage\insertRedirect
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:874
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:348
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:53
CdnCacheUpdate
Handles purging appropriate CDN URLs given a title (or titles)
Definition: CdnCacheUpdate.php:31
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:723
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:80
WikiPage\newFromID
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition: WikiPage.php:159
WikiPage\insertProtectNullRevision
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2555
$wgSitename
$wgSitename
Name of the site.
Definition: DefaultSettings.php:83
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:3653
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1703
$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:1965
Revision\RAW
const RAW
Definition: Revision.php:100
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:3088
WikiPage\getHiddenCategories
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3395
WikiPage\newFromRow
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition: WikiPage.php:186
$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:781
RequestContext\getMain
static getMain()
Static methods.
Definition: RequestContext.php:470
IP\isValid
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
WikiPage\getOldestRevision
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:583
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:596
Wikimedia\Rdbms\DBUnexpectedError
Definition: DBUnexpectedError.php:27
WikiPage\followRedirect
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:932
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:418
$wgCascadingRestrictionLevels
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
Definition: DefaultSettings.php:5334
WikiPage\formatExpiry
formatExpiry( $expiry)
Definition: WikiPage.php:2598
$args
if( $line===false) $args
Definition: cdb.php:63
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:352
ContentHandler\getContentText
static getContentText(Content $content=null)
Convenience function for getting flat text from a Content object.
Definition: ContentHandler.php:79
User\isAllowedAny
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3535
wfReadOnlyReason
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
Definition: GlobalFunctions.php:1337
$dbr
if(! $regexes) $dbr
Definition: cleanup.php:94
$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:2581
WikiPage\doEditUpdates
doEditUpdates(Revision $revision, User $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2127
$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:1965
ObjectCache\getMainWANInstance
static getMainWANInstance()
Get the main WAN cache object.
Definition: ObjectCache.php:370
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:8123
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
$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:1750
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:3640
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:1980
Revision\loadFromTimestamp
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:309
WikiPage\convertSelectType
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:198
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:3440
RecentChangesUpdateJob\newPurgeJob
static newPurgeJob()
Definition: RecentChangesUpdateJob.php:44
WikiPage\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache)
Definition: WikiPage.php:3662
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:400
WikiPage\pageData
pageData( $dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:323
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:1715
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:3627
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:979
WikiPage\getLastPurgeTimestamp
getLastPurgeTimestamp()
Get the last time a user explicitly purged the page via action=purge.
Definition: WikiPage.php:1150
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:1190
$wgArticleCountMethod
$wgArticleCountMethod
Method used to determine if a page in a content namespace should be counted as a valid article.
Definition: DefaultSettings.php:4460
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:1344
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:899
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:677
WikiPage\updateRedirectOn
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1283
WikiPage\doModify
doModify(Content $content, $flags, User $user, $summary, array $meta)
Definition: WikiPage.php:1651
WikiPage\$mLinksUpdated
string $mLinksUpdated
Definition: WikiPage.php:89
Revision\selectFields
static selectFields()
Return the list of revision fields that should be selected to create a new revision.
Definition: Revision.php:452
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:492
WikiPage\$mLastRevision
Revision $mLastRevision
Definition: WikiPage.php:74
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:50
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:445
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:51
ParserOutput\hasDynamicContent
hasDynamicContent()
Check whether the cache TTL was lowered due to dynamic content.
Definition: ParserOutput.php:1070
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:3596
WikiPage\pageDataFromId
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:361
WikiPage\doCreate
doCreate(Content $content, $flags, User $user, $summary, array $meta)
Definition: WikiPage.php:1825
WikiPage\getContentHandler
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:231
WikiPage\PURGE_CLUSTER_PCACHE
const PURGE_CLUSTER_PCACHE
Definition: WikiPage.php:93
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:259
Revision\DELETED_TEXT
const DELETED_TEXT
Definition: Revision.php:90
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:3184
$flags
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2801
array
the array() calling protocol came about after MediaWiki 1.4rc1.
$wgRCWatchCategoryMembership
$wgRCWatchCategoryMembership
Treat category membership changes as a RecentChange.
Definition: DefaultSettings.php:6795
User\isAllowed
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3565
WikiPage\clear
clear()
Clear the object.
Definition: WikiPage.php:247
MWExceptionHandler\logException
static logException( $e, $catcher=self::CAUGHT_BY_OTHER)
Log an exception to the exception log (if enabled).
Definition: MWExceptionHandler.php:613
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:2300
$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