MediaWiki  1.28.0
WikiPage.php
Go to the documentation of this file.
1 <?php
25 
32 class WikiPage implements Page, IDBAccessObject {
33  // Constants for $mDataLoadedFrom and related
34 
38  public $mTitle = null;
39 
43  public $mDataLoaded = false; // !< Boolean
44  public $mIsRedirect = false; // !< Boolean
45  public $mLatest = false; // !< Integer (false means "not loaded")
49  public $mPreparedEdit = false;
50 
54  protected $mId = null;
55 
59  protected $mDataLoadedFrom = self::READ_NONE;
60 
64  protected $mRedirectTarget = null;
65 
69  protected $mLastRevision = null;
70 
74  protected $mTimestamp = '';
75 
79  protected $mTouched = '19700101000000';
80 
84  protected $mLinksUpdated = '19700101000000';
85 
86  const PURGE_CDN_CACHE = 1; // purge CDN cache for page variant URLs
87  const PURGE_CLUSTER_PCACHE = 2; // purge parser cache in the local datacenter
88  const PURGE_GLOBAL_PCACHE = 4; // set page_touched to clear parser cache in all datacenters
89  const PURGE_ALL = 7;
90 
95  public function __construct( Title $title ) {
96  $this->mTitle = $title;
97  }
98 
103  public function __clone() {
104  $this->mTitle = clone $this->mTitle;
105  }
106 
115  public static function factory( Title $title ) {
116  $ns = $title->getNamespace();
117 
118  if ( $ns == NS_MEDIA ) {
119  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
120  } elseif ( $ns < 0 ) {
121  throw new MWException( "Invalid or virtual namespace $ns given." );
122  }
123 
124  $page = null;
125  if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
126  return $page;
127  }
128 
129  switch ( $ns ) {
130  case NS_FILE:
131  $page = new WikiFilePage( $title );
132  break;
133  case NS_CATEGORY:
134  $page = new WikiCategoryPage( $title );
135  break;
136  default:
137  $page = new WikiPage( $title );
138  }
139 
140  return $page;
141  }
142 
153  public static function newFromID( $id, $from = 'fromdb' ) {
154  // page id's are never 0 or negative, see bug 61166
155  if ( $id < 1 ) {
156  return null;
157  }
158 
159  $from = self::convertSelectType( $from );
160  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
161  $row = $db->selectRow(
162  'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
163  if ( !$row ) {
164  return null;
165  }
166  return self::newFromRow( $row, $from );
167  }
168 
180  public static function newFromRow( $row, $from = 'fromdb' ) {
181  $page = self::factory( Title::newFromRow( $row ) );
182  $page->loadFromRow( $row, $from );
183  return $page;
184  }
185 
192  private static function convertSelectType( $type ) {
193  switch ( $type ) {
194  case 'fromdb':
195  return self::READ_NORMAL;
196  case 'fromdbmaster':
197  return self::READ_LATEST;
198  case 'forupdate':
199  return self::READ_LOCKING;
200  default:
201  // It may already be an integer or whatever else
202  return $type;
203  }
204  }
205 
211  public function getActionOverrides() {
212  return $this->getContentHandler()->getActionOverrides();
213  }
214 
224  public function getContentHandler() {
226  }
227 
232  public function getTitle() {
233  return $this->mTitle;
234  }
235 
240  public function clear() {
241  $this->mDataLoaded = false;
242  $this->mDataLoadedFrom = self::READ_NONE;
243 
244  $this->clearCacheFields();
245  }
246 
251  protected function clearCacheFields() {
252  $this->mId = null;
253  $this->mRedirectTarget = null; // Title object if set
254  $this->mLastRevision = null; // Latest revision
255  $this->mTouched = '19700101000000';
256  $this->mLinksUpdated = '19700101000000';
257  $this->mTimestamp = '';
258  $this->mIsRedirect = false;
259  $this->mLatest = false;
260  // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
261  // the requested rev ID and content against the cached one for equality. For most
262  // content types, the output should not change during the lifetime of this cache.
263  // Clearing it can cause extra parses on edit for no reason.
264  }
265 
271  public function clearPreparedEdit() {
272  $this->mPreparedEdit = false;
273  }
274 
281  public static function selectFields() {
282  global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
283 
284  $fields = [
285  'page_id',
286  'page_namespace',
287  'page_title',
288  'page_restrictions',
289  'page_is_redirect',
290  'page_is_new',
291  'page_random',
292  'page_touched',
293  'page_links_updated',
294  'page_latest',
295  'page_len',
296  ];
297 
298  if ( $wgContentHandlerUseDB ) {
299  $fields[] = 'page_content_model';
300  }
301 
302  if ( $wgPageLanguageUseDB ) {
303  $fields[] = 'page_lang';
304  }
305 
306  return $fields;
307  }
308 
316  protected function pageData( $dbr, $conditions, $options = [] ) {
317  $fields = self::selectFields();
318 
319  Hooks::run( 'ArticlePageDataBefore', [ &$this, &$fields ] );
320 
321  $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
322 
323  Hooks::run( 'ArticlePageDataAfter', [ &$this, &$row ] );
324 
325  return $row;
326  }
327 
337  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
338  return $this->pageData( $dbr, [
339  'page_namespace' => $title->getNamespace(),
340  'page_title' => $title->getDBkey() ], $options );
341  }
342 
351  public function pageDataFromId( $dbr, $id, $options = [] ) {
352  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
353  }
354 
367  public function loadPageData( $from = 'fromdb' ) {
368  $from = self::convertSelectType( $from );
369  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
370  // We already have the data from the correct location, no need to load it twice.
371  return;
372  }
373 
374  if ( is_int( $from ) ) {
375  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
376  $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
377 
378  if ( !$data
379  && $index == DB_REPLICA
380  && wfGetLB()->getServerCount() > 1
381  && wfGetLB()->hasOrMadeRecentMasterChanges()
382  ) {
383  $from = self::READ_LATEST;
384  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
385  $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
386  }
387  } else {
388  // No idea from where the caller got this data, assume replica DB.
389  $data = $from;
390  $from = self::READ_NORMAL;
391  }
392 
393  $this->loadFromRow( $data, $from );
394  }
395 
407  public function loadFromRow( $data, $from ) {
408  $lc = LinkCache::singleton();
409  $lc->clearLink( $this->mTitle );
410 
411  if ( $data ) {
412  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
413 
414  $this->mTitle->loadFromRow( $data );
415 
416  // Old-fashioned restrictions
417  $this->mTitle->loadRestrictions( $data->page_restrictions );
418 
419  $this->mId = intval( $data->page_id );
420  $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
421  $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
422  $this->mIsRedirect = intval( $data->page_is_redirect );
423  $this->mLatest = intval( $data->page_latest );
424  // Bug 37225: $latest may no longer match the cached latest Revision object.
425  // Double-check the ID of any cached latest Revision object for consistency.
426  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
427  $this->mLastRevision = null;
428  $this->mTimestamp = '';
429  }
430  } else {
431  $lc->addBadLinkObj( $this->mTitle );
432 
433  $this->mTitle->loadFromRow( false );
434 
435  $this->clearCacheFields();
436 
437  $this->mId = 0;
438  }
439 
440  $this->mDataLoaded = true;
441  $this->mDataLoadedFrom = self::convertSelectType( $from );
442  }
443 
447  public function getId() {
448  if ( !$this->mDataLoaded ) {
449  $this->loadPageData();
450  }
451  return $this->mId;
452  }
453 
457  public function exists() {
458  if ( !$this->mDataLoaded ) {
459  $this->loadPageData();
460  }
461  return $this->mId > 0;
462  }
463 
472  public function hasViewableContent() {
473  return $this->mTitle->isKnown();
474  }
475 
481  public function isRedirect() {
482  if ( !$this->mDataLoaded ) {
483  $this->loadPageData();
484  }
485 
486  return (bool)$this->mIsRedirect;
487  }
488 
499  public function getContentModel() {
500  if ( $this->exists() ) {
502 
503  return $cache->getWithSetCallback(
504  $cache->makeKey( 'page', 'content-model', $this->getLatest() ),
505  $cache::TTL_MONTH,
506  function () {
507  $rev = $this->getRevision();
508  if ( $rev ) {
509  // Look at the revision's actual content model
510  return $rev->getContentModel();
511  } else {
512  $title = $this->mTitle->getPrefixedDBkey();
513  wfWarn( "Page $title exists but has no (visible) revisions!" );
514  return $this->mTitle->getContentModel();
515  }
516  }
517  );
518  }
519 
520  // use the default model for this page
521  return $this->mTitle->getContentModel();
522  }
523 
528  public function checkTouched() {
529  if ( !$this->mDataLoaded ) {
530  $this->loadPageData();
531  }
532  return ( $this->mId && !$this->mIsRedirect );
533  }
534 
539  public function getTouched() {
540  if ( !$this->mDataLoaded ) {
541  $this->loadPageData();
542  }
543  return $this->mTouched;
544  }
545 
550  public function getLinksTimestamp() {
551  if ( !$this->mDataLoaded ) {
552  $this->loadPageData();
553  }
554  return $this->mLinksUpdated;
555  }
556 
561  public function getLatest() {
562  if ( !$this->mDataLoaded ) {
563  $this->loadPageData();
564  }
565  return (int)$this->mLatest;
566  }
567 
572  public function getOldestRevision() {
573 
574  // Try using the replica DB first, then try the master
575  $continue = 2;
576  $db = wfGetDB( DB_REPLICA );
577  $revSelectFields = Revision::selectFields();
578 
579  $row = null;
580  while ( $continue ) {
581  $row = $db->selectRow(
582  [ 'page', 'revision' ],
583  $revSelectFields,
584  [
585  'page_namespace' => $this->mTitle->getNamespace(),
586  'page_title' => $this->mTitle->getDBkey(),
587  'rev_page = page_id'
588  ],
589  __METHOD__,
590  [
591  'ORDER BY' => 'rev_timestamp ASC'
592  ]
593  );
594 
595  if ( $row ) {
596  $continue = 0;
597  } else {
598  $db = wfGetDB( DB_MASTER );
599  $continue--;
600  }
601  }
602 
603  return $row ? Revision::newFromRow( $row ) : null;
604  }
605 
610  protected function loadLastEdit() {
611  if ( $this->mLastRevision !== null ) {
612  return; // already loaded
613  }
614 
615  $latest = $this->getLatest();
616  if ( !$latest ) {
617  return; // page doesn't exist or is missing page_latest info
618  }
619 
620  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
621  // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always
622  // includes the latest changes committed. This is true even within REPEATABLE-READ
623  // transactions, where S1 normally only sees changes committed before the first S1
624  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
625  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
626  // happened after the first S1 SELECT.
627  // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
629  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
630  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
631  // Bug T93976: if page_latest was loaded from the master, fetch the
632  // revision from there as well, as it may not exist yet on a replica DB.
633  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
634  $flags = Revision::READ_LATEST;
635  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
636  } else {
637  $dbr = wfGetDB( DB_REPLICA );
638  $revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
639  }
640 
641  if ( $revision ) { // sanity
642  $this->setLastEdit( $revision );
643  }
644  }
645 
650  protected function setLastEdit( Revision $revision ) {
651  $this->mLastRevision = $revision;
652  $this->mTimestamp = $revision->getTimestamp();
653  }
654 
659  public function getRevision() {
660  $this->loadLastEdit();
661  if ( $this->mLastRevision ) {
662  return $this->mLastRevision;
663  }
664  return null;
665  }
666 
680  public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
681  $this->loadLastEdit();
682  if ( $this->mLastRevision ) {
683  return $this->mLastRevision->getContent( $audience, $user );
684  }
685  return null;
686  }
687 
700  public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
701  wfDeprecated( __METHOD__, '1.21' );
702 
703  $this->loadLastEdit();
704  if ( $this->mLastRevision ) {
705  return $this->mLastRevision->getText( $audience, $user );
706  }
707  return false;
708  }
709 
713  public function getTimestamp() {
714  // Check if the field has been filled by WikiPage::setTimestamp()
715  if ( !$this->mTimestamp ) {
716  $this->loadLastEdit();
717  }
718 
719  return wfTimestamp( TS_MW, $this->mTimestamp );
720  }
721 
727  public function setTimestamp( $ts ) {
728  $this->mTimestamp = wfTimestamp( TS_MW, $ts );
729  }
730 
740  public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
741  $this->loadLastEdit();
742  if ( $this->mLastRevision ) {
743  return $this->mLastRevision->getUser( $audience, $user );
744  } else {
745  return -1;
746  }
747  }
748 
759  public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
760  $revision = $this->getOldestRevision();
761  if ( $revision ) {
762  $userName = $revision->getUserText( $audience, $user );
763  return User::newFromName( $userName, false );
764  } else {
765  return null;
766  }
767  }
768 
778  public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
779  $this->loadLastEdit();
780  if ( $this->mLastRevision ) {
781  return $this->mLastRevision->getUserText( $audience, $user );
782  } else {
783  return '';
784  }
785  }
786 
796  public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
797  $this->loadLastEdit();
798  if ( $this->mLastRevision ) {
799  return $this->mLastRevision->getComment( $audience, $user );
800  } else {
801  return '';
802  }
803  }
804 
810  public function getMinorEdit() {
811  $this->loadLastEdit();
812  if ( $this->mLastRevision ) {
813  return $this->mLastRevision->isMinor();
814  } else {
815  return false;
816  }
817  }
818 
827  public function isCountable( $editInfo = false ) {
828  global $wgArticleCountMethod;
829 
830  if ( !$this->mTitle->isContentPage() ) {
831  return false;
832  }
833 
834  if ( $editInfo ) {
835  $content = $editInfo->pstContent;
836  } else {
837  $content = $this->getContent();
838  }
839 
840  if ( !$content || $content->isRedirect() ) {
841  return false;
842  }
843 
844  $hasLinks = null;
845 
846  if ( $wgArticleCountMethod === 'link' ) {
847  // nasty special case to avoid re-parsing to detect links
848 
849  if ( $editInfo ) {
850  // ParserOutput::getLinks() is a 2D array of page links, so
851  // to be really correct we would need to recurse in the array
852  // but the main array should only have items in it if there are
853  // links.
854  $hasLinks = (bool)count( $editInfo->output->getLinks() );
855  } else {
856  $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
857  [ 'pl_from' => $this->getId() ], __METHOD__ );
858  }
859  }
860 
861  return $content->isCountable( $hasLinks );
862  }
863 
871  public function getRedirectTarget() {
872  if ( !$this->mTitle->isRedirect() ) {
873  return null;
874  }
875 
876  if ( $this->mRedirectTarget !== null ) {
877  return $this->mRedirectTarget;
878  }
879 
880  // Query the redirect table
881  $dbr = wfGetDB( DB_REPLICA );
882  $row = $dbr->selectRow( 'redirect',
883  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
884  [ 'rd_from' => $this->getId() ],
885  __METHOD__
886  );
887 
888  // rd_fragment and rd_interwiki were added later, populate them if empty
889  if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
890  $this->mRedirectTarget = Title::makeTitle(
891  $row->rd_namespace, $row->rd_title,
892  $row->rd_fragment, $row->rd_interwiki
893  );
894  return $this->mRedirectTarget;
895  }
896 
897  // This page doesn't have an entry in the redirect table
898  $this->mRedirectTarget = $this->insertRedirect();
899  return $this->mRedirectTarget;
900  }
901 
910  public function insertRedirect() {
911  $content = $this->getContent();
912  $retval = $content ? $content->getUltimateRedirectTarget() : null;
913  if ( !$retval ) {
914  return null;
915  }
916 
917  // Update the DB post-send if the page has not cached since now
918  $that = $this;
919  $latest = $this->getLatest();
921  function () use ( $that, $retval, $latest ) {
922  $that->insertRedirectEntry( $retval, $latest );
923  },
925  wfGetDB( DB_MASTER )
926  );
927 
928  return $retval;
929  }
930 
936  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
937  $dbw = wfGetDB( DB_MASTER );
938  $dbw->startAtomic( __METHOD__ );
939 
940  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
941  $dbw->replace( 'redirect',
942  [ 'rd_from' ],
943  [
944  'rd_from' => $this->getId(),
945  'rd_namespace' => $rt->getNamespace(),
946  'rd_title' => $rt->getDBkey(),
947  'rd_fragment' => $rt->getFragment(),
948  'rd_interwiki' => $rt->getInterwiki(),
949  ],
950  __METHOD__
951  );
952  }
953 
954  $dbw->endAtomic( __METHOD__ );
955  }
956 
962  public function followRedirect() {
963  return $this->getRedirectURL( $this->getRedirectTarget() );
964  }
965 
973  public function getRedirectURL( $rt ) {
974  if ( !$rt ) {
975  return false;
976  }
977 
978  if ( $rt->isExternal() ) {
979  if ( $rt->isLocal() ) {
980  // Offsite wikis need an HTTP redirect.
981  // This can be hard to reverse and may produce loops,
982  // so they may be disabled in the site configuration.
983  $source = $this->mTitle->getFullURL( 'redirect=no' );
984  return $rt->getFullURL( [ 'rdfrom' => $source ] );
985  } else {
986  // External pages without "local" bit set are not valid
987  // redirect targets
988  return false;
989  }
990  }
991 
992  if ( $rt->isSpecialPage() ) {
993  // Gotta handle redirects to special pages differently:
994  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
995  // Some pages are not valid targets.
996  if ( $rt->isValidRedirectTarget() ) {
997  return $rt->getFullURL();
998  } else {
999  return false;
1000  }
1001  }
1002 
1003  return $rt;
1004  }
1005 
1011  public function getContributors() {
1012  // @todo FIXME: This is expensive; cache this info somewhere.
1013 
1014  $dbr = wfGetDB( DB_REPLICA );
1015 
1016  if ( $dbr->implicitGroupby() ) {
1017  $realNameField = 'user_real_name';
1018  } else {
1019  $realNameField = 'MIN(user_real_name) AS user_real_name';
1020  }
1021 
1022  $tables = [ 'revision', 'user' ];
1023 
1024  $fields = [
1025  'user_id' => 'rev_user',
1026  'user_name' => 'rev_user_text',
1027  $realNameField,
1028  'timestamp' => 'MAX(rev_timestamp)',
1029  ];
1030 
1031  $conds = [ 'rev_page' => $this->getId() ];
1032 
1033  // The user who made the top revision gets credited as "this page was last edited by
1034  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1035  $user = $this->getUser();
1036  if ( $user ) {
1037  $conds[] = "rev_user != $user";
1038  } else {
1039  $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
1040  }
1041 
1042  // Username hidden?
1043  $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1044 
1045  $jconds = [
1046  'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
1047  ];
1048 
1049  $options = [
1050  'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
1051  'ORDER BY' => 'timestamp DESC',
1052  ];
1053 
1054  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1055  return new UserArrayFromResult( $res );
1056  }
1057 
1065  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1066  return $parserOptions->getStubThreshold() == 0
1067  && $this->exists()
1068  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1069  && $this->getContentHandler()->isParserCacheSupported();
1070  }
1071 
1085  public function getParserOutput(
1086  ParserOptions $parserOptions, $oldid = null, $forceParse = false
1087  ) {
1088  $useParserCache =
1089  ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1090  wfDebug( __METHOD__ .
1091  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1092  if ( $parserOptions->getStubThreshold() ) {
1093  wfIncrStats( 'pcache.miss.stub' );
1094  }
1095 
1096  if ( $useParserCache ) {
1097  $parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
1098  if ( $parserOutput !== false ) {
1099  return $parserOutput;
1100  }
1101  }
1102 
1103  if ( $oldid === null || $oldid === 0 ) {
1104  $oldid = $this->getLatest();
1105  }
1106 
1107  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1108  $pool->execute();
1109 
1110  return $pool->getParserOutput();
1111  }
1112 
1118  public function doViewUpdates( User $user, $oldid = 0 ) {
1119  if ( wfReadOnly() ) {
1120  return;
1121  }
1122 
1123  Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1124  // Update newtalk / watchlist notification status
1125  try {
1126  $user->clearNotification( $this->mTitle, $oldid );
1127  } catch ( DBError $e ) {
1128  // Avoid outage if the master is not reachable
1130  }
1131  }
1132 
1138  public function doPurge( $flags = self::PURGE_ALL ) {
1139  if ( !Hooks::run( 'ArticlePurge', [ &$this ] ) ) {
1140  return false;
1141  }
1142 
1143  if ( ( $flags & self::PURGE_GLOBAL_PCACHE ) == self::PURGE_GLOBAL_PCACHE ) {
1144  // Set page_touched in the database to invalidate all DC caches
1145  $this->mTitle->invalidateCache();
1146  } elseif ( ( $flags & self::PURGE_CLUSTER_PCACHE ) == self::PURGE_CLUSTER_PCACHE ) {
1147  // Delete the parser options key in the local cluster to invalidate the DC cache
1148  ParserCache::singleton()->deleteOptionsKey( $this );
1149  // Avoid sending HTTP 304s in ViewAction to the client who just issued the purge
1151  $cache->set(
1152  $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ),
1153  wfTimestamp( TS_MW ),
1154  $cache::TTL_HOUR
1155  );
1156  }
1157 
1158  if ( ( $flags & self::PURGE_CDN_CACHE ) == self::PURGE_CDN_CACHE ) {
1159  // Clear any HTML file cache
1161  // Send purge after any page_touched above update was committed
1163  new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1165  );
1166  }
1167 
1168  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1169  // @todo move this logic to MessageCache
1170  if ( $this->exists() ) {
1171  // NOTE: use transclusion text for messages.
1172  // This is consistent with MessageCache::getMsgFromNamespace()
1173 
1174  $content = $this->getContent();
1175  $text = $content === null ? null : $content->getWikitextForTransclusion();
1176 
1177  if ( $text === null ) {
1178  $text = false;
1179  }
1180  } else {
1181  $text = false;
1182  }
1183 
1184  MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
1185  }
1186 
1187  return true;
1188  }
1189 
1196  public function getLastPurgeTimestamp() {
1198 
1199  return $cache->get( $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ) );
1200  }
1201 
1216  public function insertOn( $dbw, $pageId = null ) {
1217  $pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
1218  $dbw->insert(
1219  'page',
1220  [
1221  'page_id' => $pageIdForInsert,
1222  'page_namespace' => $this->mTitle->getNamespace(),
1223  'page_title' => $this->mTitle->getDBkey(),
1224  'page_restrictions' => '',
1225  'page_is_redirect' => 0, // Will set this shortly...
1226  'page_is_new' => 1,
1227  'page_random' => wfRandom(),
1228  'page_touched' => $dbw->timestamp(),
1229  'page_latest' => 0, // Fill this in shortly...
1230  'page_len' => 0, // Fill this in shortly...
1231  ],
1232  __METHOD__,
1233  'IGNORE'
1234  );
1235 
1236  if ( $dbw->affectedRows() > 0 ) {
1237  $newid = $pageId ?: $dbw->insertId();
1238  $this->mId = $newid;
1239  $this->mTitle->resetArticleID( $newid );
1240 
1241  return $newid;
1242  } else {
1243  return false; // nothing changed
1244  }
1245  }
1246 
1260  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1261  $lastRevIsRedirect = null
1262  ) {
1263  global $wgContentHandlerUseDB;
1264 
1265  // Assertion to try to catch T92046
1266  if ( (int)$revision->getId() === 0 ) {
1267  throw new InvalidArgumentException(
1268  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1269  );
1270  }
1271 
1272  $content = $revision->getContent();
1273  $len = $content ? $content->getSize() : 0;
1274  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1275 
1276  $conditions = [ 'page_id' => $this->getId() ];
1277 
1278  if ( !is_null( $lastRevision ) ) {
1279  // An extra check against threads stepping on each other
1280  $conditions['page_latest'] = $lastRevision;
1281  }
1282 
1283  $row = [ /* SET */
1284  'page_latest' => $revision->getId(),
1285  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1286  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1287  'page_is_redirect' => $rt !== null ? 1 : 0,
1288  'page_len' => $len,
1289  ];
1290 
1291  if ( $wgContentHandlerUseDB ) {
1292  $row['page_content_model'] = $revision->getContentModel();
1293  }
1294 
1295  $dbw->update( 'page',
1296  $row,
1297  $conditions,
1298  __METHOD__ );
1299 
1300  $result = $dbw->affectedRows() > 0;
1301  if ( $result ) {
1302  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1303  $this->setLastEdit( $revision );
1304  $this->mLatest = $revision->getId();
1305  $this->mIsRedirect = (bool)$rt;
1306  // Update the LinkCache.
1307  LinkCache::singleton()->addGoodLinkObj(
1308  $this->getId(),
1309  $this->mTitle,
1310  $len,
1311  $this->mIsRedirect,
1312  $this->mLatest,
1313  $revision->getContentModel()
1314  );
1315  }
1316 
1317  return $result;
1318  }
1319 
1331  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1332  // Always update redirects (target link might have changed)
1333  // Update/Insert if we don't know if the last revision was a redirect or not
1334  // Delete if changing from redirect to non-redirect
1335  $isRedirect = !is_null( $redirectTitle );
1336 
1337  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1338  return true;
1339  }
1340 
1341  if ( $isRedirect ) {
1342  $this->insertRedirectEntry( $redirectTitle );
1343  } else {
1344  // This is not a redirect, remove row from redirect table
1345  $where = [ 'rd_from' => $this->getId() ];
1346  $dbw->delete( 'redirect', $where, __METHOD__ );
1347  }
1348 
1349  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1350  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1351  }
1352 
1353  return ( $dbw->affectedRows() != 0 );
1354  }
1355 
1366  public function updateIfNewerOn( $dbw, $revision ) {
1367 
1368  $row = $dbw->selectRow(
1369  [ 'revision', 'page' ],
1370  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1371  [
1372  'page_id' => $this->getId(),
1373  'page_latest=rev_id' ],
1374  __METHOD__ );
1375 
1376  if ( $row ) {
1377  if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1378  return false;
1379  }
1380  $prev = $row->rev_id;
1381  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1382  } else {
1383  // No or missing previous revision; mark the page as new
1384  $prev = 0;
1385  $lastRevIsRedirect = null;
1386  }
1387 
1388  $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1389 
1390  return $ret;
1391  }
1392 
1403  public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
1404  $handler = $undo->getContentHandler();
1405  return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1406  }
1407 
1418  public function supportsSections() {
1419  return $this->getContentHandler()->supportsSections();
1420  }
1421 
1436  public function replaceSectionContent(
1437  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1438  ) {
1439 
1440  $baseRevId = null;
1441  if ( $edittime && $sectionId !== 'new' ) {
1442  $dbr = wfGetDB( DB_REPLICA );
1443  $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1444  // Try the master if this thread may have just added it.
1445  // This could be abstracted into a Revision method, but we don't want
1446  // to encourage loading of revisions by timestamp.
1447  if ( !$rev
1448  && wfGetLB()->getServerCount() > 1
1449  && wfGetLB()->hasOrMadeRecentMasterChanges()
1450  ) {
1451  $dbw = wfGetDB( DB_MASTER );
1452  $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1453  }
1454  if ( $rev ) {
1455  $baseRevId = $rev->getId();
1456  }
1457  }
1458 
1459  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1460  }
1461 
1475  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1476  $sectionTitle = '', $baseRevId = null
1477  ) {
1478 
1479  if ( strval( $sectionId ) === '' ) {
1480  // Whole-page edit; let the whole text through
1481  $newContent = $sectionContent;
1482  } else {
1483  if ( !$this->supportsSections() ) {
1484  throw new MWException( "sections not supported for content model " .
1485  $this->getContentHandler()->getModelID() );
1486  }
1487 
1488  // Bug 30711: always use current version when adding a new section
1489  if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1490  $oldContent = $this->getContent();
1491  } else {
1492  $rev = Revision::newFromId( $baseRevId );
1493  if ( !$rev ) {
1494  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1495  $this->getId() . "; section: $sectionId)\n" );
1496  return null;
1497  }
1498 
1499  $oldContent = $rev->getContent();
1500  }
1501 
1502  if ( !$oldContent ) {
1503  wfDebug( __METHOD__ . ": no page text\n" );
1504  return null;
1505  }
1506 
1507  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1508  }
1509 
1510  return $newContent;
1511  }
1512 
1518  public function checkFlags( $flags ) {
1519  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1520  if ( $this->exists() ) {
1521  $flags |= EDIT_UPDATE;
1522  } else {
1523  $flags |= EDIT_NEW;
1524  }
1525  }
1526 
1527  return $flags;
1528  }
1529 
1584  public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
1585  wfDeprecated( __METHOD__, '1.21' );
1586 
1587  $content = ContentHandler::makeContent( $text, $this->getTitle() );
1588 
1589  return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
1590  }
1591 
1649  public function doEditContent(
1650  Content $content, $summary, $flags = 0, $baseRevId = false,
1651  User $user = null, $serialFormat = null, $tags = []
1652  ) {
1653  global $wgUser, $wgUseAutomaticEditSummaries;
1654 
1655  // Old default parameter for $tags was null
1656  if ( $tags === null ) {
1657  $tags = [];
1658  }
1659 
1660  // Low-level sanity check
1661  if ( $this->mTitle->getText() === '' ) {
1662  throw new MWException( 'Something is trying to edit an article with an empty title' );
1663  }
1664  // Make sure the given content type is allowed for this page
1665  if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
1666  return Status::newFatal( 'content-not-allowed-here',
1668  $this->mTitle->getPrefixedText()
1669  );
1670  }
1671 
1672  // Load the data from the master database if needed.
1673  // The caller may already loaded it from the master or even loaded it using
1674  // SELECT FOR UPDATE, so do not override that using clear().
1675  $this->loadPageData( 'fromdbmaster' );
1676 
1677  $user = $user ?: $wgUser;
1678  $flags = $this->checkFlags( $flags );
1679 
1680  // Trigger pre-save hook (using provided edit summary)
1681  $hookStatus = Status::newGood( [] );
1682  $hook_args = [ &$this, &$user, &$content, &$summary,
1683  $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
1684  // Check if the hook rejected the attempted save
1685  if ( !Hooks::run( 'PageContentSave', $hook_args )
1686  || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args, '1.21' )
1687  ) {
1688  if ( $hookStatus->isOK() ) {
1689  // Hook returned false but didn't call fatal(); use generic message
1690  $hookStatus->fatal( 'edit-hook-aborted' );
1691  }
1692 
1693  return $hookStatus;
1694  }
1695 
1696  $old_revision = $this->getRevision(); // current revision
1697  $old_content = $this->getContent( Revision::RAW ); // current revision's content
1698 
1699  if ( $old_content && $old_content->getModel() !== $content->getModel() ) {
1700  $tags[] = 'mw-contentmodelchange';
1701  }
1702 
1703  // Provide autosummaries if one is not provided and autosummaries are enabled
1704  if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
1705  $handler = $content->getContentHandler();
1706  $summary = $handler->getAutosummary( $old_content, $content, $flags );
1707  }
1708 
1709  // Avoid statsd noise and wasted cycles check the edit stash (T136678)
1710  if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
1711  $useCache = false;
1712  } else {
1713  $useCache = true;
1714  }
1715 
1716  // Get the pre-save transform content and final parser output
1717  $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat, $useCache );
1718  $pstContent = $editInfo->pstContent; // Content object
1719  $meta = [
1720  'bot' => ( $flags & EDIT_FORCE_BOT ),
1721  'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
1722  'serialized' => $editInfo->pst,
1723  'serialFormat' => $serialFormat,
1724  'baseRevId' => $baseRevId,
1725  'oldRevision' => $old_revision,
1726  'oldContent' => $old_content,
1727  'oldId' => $this->getLatest(),
1728  'oldIsRedirect' => $this->isRedirect(),
1729  'oldCountable' => $this->isCountable(),
1730  'tags' => ( $tags !== null ) ? (array)$tags : []
1731  ];
1732 
1733  // Actually create the revision and create/update the page
1734  if ( $flags & EDIT_UPDATE ) {
1735  $status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
1736  } else {
1737  $status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
1738  }
1739 
1740  // Promote user to any groups they meet the criteria for
1741  DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1742  $user->addAutopromoteOnceGroups( 'onEdit' );
1743  $user->addAutopromoteOnceGroups( 'onView' ); // b/c
1744  } );
1745 
1746  return $status;
1747  }
1748 
1761  private function doModify(
1763  ) {
1764  global $wgUseRCPatrol;
1765 
1766  // Update article, but only if changed.
1767  $status = Status::newGood( [ 'new' => false, 'revision' => null ] );
1768 
1769  // Convenience variables
1770  $now = wfTimestampNow();
1771  $oldid = $meta['oldId'];
1773  $oldContent = $meta['oldContent'];
1774  $newsize = $content->getSize();
1775 
1776  if ( !$oldid ) {
1777  // Article gone missing
1778  $status->fatal( 'edit-gone-missing' );
1779 
1780  return $status;
1781  } elseif ( !$oldContent ) {
1782  // Sanity check for bug 37225
1783  throw new MWException( "Could not find text for current revision {$oldid}." );
1784  }
1785 
1786  // @TODO: pass content object?!
1787  $revision = new Revision( [
1788  'page' => $this->getId(),
1789  'title' => $this->mTitle, // for determining the default content model
1790  'comment' => $summary,
1791  'minor_edit' => $meta['minor'],
1792  'text' => $meta['serialized'],
1793  'len' => $newsize,
1794  'parent_id' => $oldid,
1795  'user' => $user->getId(),
1796  'user_text' => $user->getName(),
1797  'timestamp' => $now,
1798  'content_model' => $content->getModel(),
1799  'content_format' => $meta['serialFormat'],
1800  ] );
1801 
1802  $changed = !$content->equals( $oldContent );
1803 
1804  $dbw = wfGetDB( DB_MASTER );
1805 
1806  if ( $changed ) {
1807  $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
1808  $status->merge( $prepStatus );
1809  if ( !$status->isOK() ) {
1810  return $status;
1811  }
1812 
1813  $dbw->startAtomic( __METHOD__ );
1814  // Get the latest page_latest value while locking it.
1815  // Do a CAS style check to see if it's the same as when this method
1816  // started. If it changed then bail out before touching the DB.
1817  $latestNow = $this->lockAndGetLatest();
1818  if ( $latestNow != $oldid ) {
1819  $dbw->endAtomic( __METHOD__ );
1820  // Page updated or deleted in the mean time
1821  $status->fatal( 'edit-conflict' );
1822 
1823  return $status;
1824  }
1825 
1826  // At this point we are now comitted to returning an OK
1827  // status unless some DB query error or other exception comes up.
1828  // This way callers don't have to call rollback() if $status is bad
1829  // unless they actually try to catch exceptions (which is rare).
1830 
1831  // Save the revision text
1832  $revisionId = $revision->insertOn( $dbw );
1833  // Update page_latest and friends to reflect the new revision
1834  if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
1835  throw new MWException( "Failed to update page row to use new revision." );
1836  }
1837 
1838  Hooks::run( 'NewRevisionFromEditComplete',
1839  [ $this, $revision, $meta['baseRevId'], $user ] );
1840 
1841  // Update recentchanges
1842  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1843  // Mark as patrolled if the user can do so
1844  $patrolled = $wgUseRCPatrol && !count(
1845  $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1846  // Add RC row to the DB
1848  $now,
1849  $this->mTitle,
1850  $revision->isMinor(),
1851  $user,
1852  $summary,
1853  $oldid,
1854  $this->getTimestamp(),
1855  $meta['bot'],
1856  '',
1857  $oldContent ? $oldContent->getSize() : 0,
1858  $newsize,
1859  $revisionId,
1860  $patrolled,
1861  $meta['tags']
1862  );
1863  }
1864 
1865  $user->incEditCount();
1866 
1867  $dbw->endAtomic( __METHOD__ );
1868  $this->mTimestamp = $now;
1869  } else {
1870  // Bug 32948: revision ID must be set to page {{REVISIONID}} and
1871  // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
1872  $revision->setId( $this->getLatest() );
1873  $revision->setUserIdAndName(
1874  $this->getUser( Revision::RAW ),
1875  $this->getUserText( Revision::RAW )
1876  );
1877  }
1878 
1879  if ( $changed ) {
1880  // Return the new revision to the caller
1881  $status->value['revision'] = $revision;
1882  } else {
1883  $status->warning( 'edit-no-change' );
1884  // Update page_touched as updateRevisionOn() was not called.
1885  // Other cache updates are managed in onArticleEdit() via doEditUpdates().
1886  $this->mTitle->invalidateCache( $now );
1887  }
1888 
1889  // Do secondary updates once the main changes have been committed...
1891  new AtomicSectionUpdate(
1892  $dbw,
1893  __METHOD__,
1894  function () use (
1895  $revision, &$user, $content, $summary, &$flags,
1896  $changed, $meta, &$status
1897  ) {
1898  // Update links tables, site stats, etc.
1899  $this->doEditUpdates(
1900  $revision,
1901  $user,
1902  [
1903  'changed' => $changed,
1904  'oldcountable' => $meta['oldCountable'],
1905  'oldrevision' => $meta['oldRevision']
1906  ]
1907  );
1908  // Trigger post-save hook
1909  $params = [ &$this, &$user, $content, $summary, $flags & EDIT_MINOR,
1910  null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
1911  ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1912  Hooks::run( 'PageContentSaveComplete', $params );
1913  }
1914  ),
1916  );
1917 
1918  return $status;
1919  }
1920 
1933  private function doCreate(
1935  ) {
1936  global $wgUseRCPatrol, $wgUseNPPatrol;
1937 
1938  $status = Status::newGood( [ 'new' => true, 'revision' => null ] );
1939 
1940  $now = wfTimestampNow();
1941  $newsize = $content->getSize();
1942  $prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
1943  $status->merge( $prepStatus );
1944  if ( !$status->isOK() ) {
1945  return $status;
1946  }
1947 
1948  $dbw = wfGetDB( DB_MASTER );
1949  $dbw->startAtomic( __METHOD__ );
1950 
1951  // Add the page record unless one already exists for the title
1952  $newid = $this->insertOn( $dbw );
1953  if ( $newid === false ) {
1954  $dbw->endAtomic( __METHOD__ ); // nothing inserted
1955  $status->fatal( 'edit-already-exists' );
1956 
1957  return $status; // nothing done
1958  }
1959 
1960  // At this point we are now comitted to returning an OK
1961  // status unless some DB query error or other exception comes up.
1962  // This way callers don't have to call rollback() if $status is bad
1963  // unless they actually try to catch exceptions (which is rare).
1964 
1965  // @TODO: pass content object?!
1966  $revision = new Revision( [
1967  'page' => $newid,
1968  'title' => $this->mTitle, // for determining the default content model
1969  'comment' => $summary,
1970  'minor_edit' => $meta['minor'],
1971  'text' => $meta['serialized'],
1972  'len' => $newsize,
1973  'user' => $user->getId(),
1974  'user_text' => $user->getName(),
1975  'timestamp' => $now,
1976  'content_model' => $content->getModel(),
1977  'content_format' => $meta['serialFormat'],
1978  ] );
1979 
1980  // Save the revision text...
1981  $revisionId = $revision->insertOn( $dbw );
1982  // Update the page record with revision data
1983  if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
1984  throw new MWException( "Failed to update page row to use new revision." );
1985  }
1986 
1987  Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
1988 
1989  // Update recentchanges
1990  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1991  // Mark as patrolled if the user can do so
1992  $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
1993  !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1994  // Add RC row to the DB
1996  $now,
1997  $this->mTitle,
1998  $revision->isMinor(),
1999  $user,
2000  $summary,
2001  $meta['bot'],
2002  '',
2003  $newsize,
2004  $revisionId,
2005  $patrolled,
2006  $meta['tags']
2007  );
2008  }
2009 
2010  $user->incEditCount();
2011 
2012  $dbw->endAtomic( __METHOD__ );
2013  $this->mTimestamp = $now;
2014 
2015  // Return the new revision to the caller
2016  $status->value['revision'] = $revision;
2017 
2018  // Do secondary updates once the main changes have been committed...
2020  new AtomicSectionUpdate(
2021  $dbw,
2022  __METHOD__,
2023  function () use (
2024  $revision, &$user, $content, $summary, &$flags, $meta, &$status
2025  ) {
2026  // Update links, etc.
2027  $this->doEditUpdates( $revision, $user, [ 'created' => true ] );
2028  // Trigger post-create hook
2029  $params = [ &$this, &$user, $content, $summary,
2030  $flags & EDIT_MINOR, null, null, &$flags, $revision ];
2031  ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params, '1.21' );
2032  Hooks::run( 'PageContentInsertComplete', $params );
2033  // Trigger post-save hook
2034  $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
2035  ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params, '1.21' );
2036  Hooks::run( 'PageContentSaveComplete', $params );
2037 
2038  }
2039  ),
2041  );
2042 
2043  return $status;
2044  }
2045 
2060  public function makeParserOptions( $context ) {
2061  $options = $this->getContentHandler()->makeParserOptions( $context );
2062 
2063  if ( $this->getTitle()->isConversionTable() ) {
2064  // @todo ConversionTable should become a separate content model, so
2065  // we don't need special cases like this one.
2066  $options->disableContentConversion();
2067  }
2068 
2069  return $options;
2070  }
2071 
2082  public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
2083  wfDeprecated( __METHOD__, '1.21' );
2084  $content = ContentHandler::makeContent( $text, $this->getTitle() );
2085  return $this->prepareContentForEdit( $content, $revid, $user );
2086  }
2087 
2103  public function prepareContentForEdit(
2104  Content $content, $revision = null, User $user = null,
2105  $serialFormat = null, $useCache = true
2106  ) {
2107  global $wgContLang, $wgUser, $wgAjaxEditStash;
2108 
2109  if ( is_object( $revision ) ) {
2110  $revid = $revision->getId();
2111  } else {
2112  $revid = $revision;
2113  // This code path is deprecated, and nothing is known to
2114  // use it, so performance here shouldn't be a worry.
2115  if ( $revid !== null ) {
2116  $revision = Revision::newFromId( $revid, Revision::READ_LATEST );
2117  } else {
2118  $revision = null;
2119  }
2120  }
2121 
2122  $user = is_null( $user ) ? $wgUser : $user;
2123  // XXX: check $user->getId() here???
2124 
2125  // Use a sane default for $serialFormat, see bug 57026
2126  if ( $serialFormat === null ) {
2127  $serialFormat = $content->getContentHandler()->getDefaultFormat();
2128  }
2129 
2130  if ( $this->mPreparedEdit
2131  && isset( $this->mPreparedEdit->newContent )
2132  && $this->mPreparedEdit->newContent->equals( $content )
2133  && $this->mPreparedEdit->revid == $revid
2134  && $this->mPreparedEdit->format == $serialFormat
2135  // XXX: also check $user here?
2136  ) {
2137  // Already prepared
2138  return $this->mPreparedEdit;
2139  }
2140 
2141  // The edit may have already been prepared via api.php?action=stashedit
2142  $cachedEdit = $useCache && $wgAjaxEditStash
2143  ? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
2144  : false;
2145 
2146  $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
2147  Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
2148 
2149  $edit = (object)[];
2150  if ( $cachedEdit ) {
2151  $edit->timestamp = $cachedEdit->timestamp;
2152  } else {
2153  $edit->timestamp = wfTimestampNow();
2154  }
2155  // @note: $cachedEdit is safely not used if the rev ID was referenced in the text
2156  $edit->revid = $revid;
2157 
2158  if ( $cachedEdit ) {
2159  $edit->pstContent = $cachedEdit->pstContent;
2160  } else {
2161  $edit->pstContent = $content
2162  ? $content->preSaveTransform( $this->mTitle, $user, $popts )
2163  : null;
2164  }
2165 
2166  $edit->format = $serialFormat;
2167  $edit->popts = $this->makeParserOptions( 'canonical' );
2168  if ( $cachedEdit ) {
2169  $edit->output = $cachedEdit->output;
2170  } else {
2171  if ( $revision ) {
2172  // We get here if vary-revision is set. This means that this page references
2173  // itself (such as via self-transclusion). In this case, we need to make sure
2174  // that any such self-references refer to the newly-saved revision, and not
2175  // to the previous one, which could otherwise happen due to replica DB lag.
2176  $oldCallback = $edit->popts->getCurrentRevisionCallback();
2177  $edit->popts->setCurrentRevisionCallback(
2178  function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
2179  if ( $title->equals( $revision->getTitle() ) ) {
2180  return $revision;
2181  } else {
2182  return call_user_func( $oldCallback, $title, $parser );
2183  }
2184  }
2185  );
2186  } else {
2187  // Try to avoid a second parse if {{REVISIONID}} is used
2188  $edit->popts->setSpeculativeRevIdCallback( function () {
2189  return 1 + (int)wfGetDB( DB_MASTER )->selectField(
2190  'revision',
2191  'MAX(rev_id)',
2192  [],
2193  __METHOD__
2194  );
2195  } );
2196  }
2197  $edit->output = $edit->pstContent
2198  ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2199  : null;
2200  }
2201 
2202  $edit->newContent = $content;
2203  $edit->oldContent = $this->getContent( Revision::RAW );
2204 
2205  // NOTE: B/C for hooks! don't use these fields!
2206  $edit->newText = $edit->newContent
2207  ? ContentHandler::getContentText( $edit->newContent )
2208  : '';
2209  $edit->oldText = $edit->oldContent
2210  ? ContentHandler::getContentText( $edit->oldContent )
2211  : '';
2212  $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
2213 
2214  if ( $edit->output ) {
2215  $edit->output->setCacheTime( wfTimestampNow() );
2216  }
2217 
2218  // Process cache the result
2219  $this->mPreparedEdit = $edit;
2220 
2221  return $edit;
2222  }
2223 
2245  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2246  global $wgRCWatchCategoryMembership, $wgContLang;
2247 
2248  $options += [
2249  'changed' => true,
2250  'created' => false,
2251  'moved' => false,
2252  'restored' => false,
2253  'oldrevision' => null,
2254  'oldcountable' => null
2255  ];
2256  $content = $revision->getContent();
2257 
2258  $logger = LoggerFactory::getInstance( 'SaveParse' );
2259 
2260  // See if the parser output before $revision was inserted is still valid
2261  $editInfo = false;
2262  if ( !$this->mPreparedEdit ) {
2263  $logger->debug( __METHOD__ . ": No prepared edit...\n" );
2264  } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
2265  $logger->info( __METHOD__ . ": Prepared edit has vary-revision...\n" );
2266  } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision-id' )
2267  && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()
2268  ) {
2269  $logger->info( __METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n" );
2270  } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-user' ) && !$options['changed'] ) {
2271  $logger->info( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
2272  } else {
2273  wfDebug( __METHOD__ . ": Using prepared edit...\n" );
2274  $editInfo = $this->mPreparedEdit;
2275  }
2276 
2277  if ( !$editInfo ) {
2278  // Parse the text again if needed. Be careful not to do pre-save transform twice:
2279  // $text is usually already pre-save transformed once. Avoid using the edit stash
2280  // as any prepared content from there or in doEditContent() was already rejected.
2281  $editInfo = $this->prepareContentForEdit( $content, $revision, $user, null, false );
2282  }
2283 
2284  // Save it to the parser cache.
2285  // Make sure the cache time matches page_touched to avoid double parsing.
2286  ParserCache::singleton()->save(
2287  $editInfo->output, $this, $editInfo->popts,
2288  $revision->getTimestamp(), $editInfo->revid
2289  );
2290 
2291  // Update the links tables and other secondary data
2292  if ( $content ) {
2293  $recursive = $options['changed']; // bug 50785
2294  $updates = $content->getSecondaryDataUpdates(
2295  $this->getTitle(), null, $recursive, $editInfo->output
2296  );
2297  foreach ( $updates as $update ) {
2298  if ( $update instanceof LinksUpdate ) {
2299  $update->setRevision( $revision );
2300  $update->setTriggeringUser( $user );
2301  }
2302  DeferredUpdates::addUpdate( $update );
2303  }
2304  if ( $wgRCWatchCategoryMembership
2305  && $this->getContentHandler()->supportsCategories() === true
2306  && ( $options['changed'] || $options['created'] )
2307  && !$options['restored']
2308  ) {
2309  // Note: jobs are pushed after deferred updates, so the job should be able to see
2310  // the recent change entry (also done via deferred updates) and carry over any
2311  // bot/deletion/IP flags, ect.
2313  $this->getTitle(),
2314  [
2315  'pageId' => $this->getId(),
2316  'revTimestamp' => $revision->getTimestamp()
2317  ]
2318  ) );
2319  }
2320  }
2321 
2322  Hooks::run( 'ArticleEditUpdates', [ &$this, &$editInfo, $options['changed'] ] );
2323 
2324  if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$this ] ) ) {
2325  // Flush old entries from the `recentchanges` table
2326  if ( mt_rand( 0, 9 ) == 0 ) {
2328  }
2329  }
2330 
2331  if ( !$this->exists() ) {
2332  return;
2333  }
2334 
2335  $id = $this->getId();
2336  $title = $this->mTitle->getPrefixedDBkey();
2337  $shortTitle = $this->mTitle->getDBkey();
2338 
2339  if ( $options['oldcountable'] === 'no-change' ||
2340  ( !$options['changed'] && !$options['moved'] )
2341  ) {
2342  $good = 0;
2343  } elseif ( $options['created'] ) {
2344  $good = (int)$this->isCountable( $editInfo );
2345  } elseif ( $options['oldcountable'] !== null ) {
2346  $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
2347  } else {
2348  $good = 0;
2349  }
2350  $edits = $options['changed'] ? 1 : 0;
2351  $total = $options['created'] ? 1 : 0;
2352 
2353  DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
2355 
2356  // If this is another user's talk page, update newtalk.
2357  // Don't do this if $options['changed'] = false (null-edits) nor if
2358  // it's a minor edit and the user doesn't want notifications for those.
2359  if ( $options['changed']
2360  && $this->mTitle->getNamespace() == NS_USER_TALK
2361  && $shortTitle != $user->getTitleKey()
2362  && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
2363  ) {
2364  $recipient = User::newFromName( $shortTitle, false );
2365  if ( !$recipient ) {
2366  wfDebug( __METHOD__ . ": invalid username\n" );
2367  } else {
2368  // Allow extensions to prevent user notification
2369  // when a new message is added to their talk page
2370  if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$this, $recipient ] ) ) {
2371  if ( User::isIP( $shortTitle ) ) {
2372  // An anonymous user
2373  $recipient->setNewtalk( true, $revision );
2374  } elseif ( $recipient->isLoggedIn() ) {
2375  $recipient->setNewtalk( true, $revision );
2376  } else {
2377  wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
2378  }
2379  }
2380  }
2381  }
2382 
2383  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
2384  // XXX: could skip pseudo-messages like js/css here, based on content model.
2385  $msgtext = $content ? $content->getWikitextForTransclusion() : null;
2386  if ( $msgtext === false || $msgtext === null ) {
2387  $msgtext = '';
2388  }
2389 
2390  MessageCache::singleton()->replace( $shortTitle, $msgtext );
2391 
2392  if ( $wgContLang->hasVariants() ) {
2393  $wgContLang->updateConversionTable( $this->mTitle );
2394  }
2395  }
2396 
2397  if ( $options['created'] ) {
2398  self::onArticleCreate( $this->mTitle );
2399  } elseif ( $options['changed'] ) { // bug 50785
2400  self::onArticleEdit( $this->mTitle, $revision );
2401  }
2402 
2404  $this->mTitle, $options['oldrevision'], $revision, wfWikiID()
2405  );
2406  }
2407 
2422  public function doUpdateRestrictions( array $limit, array $expiry,
2423  &$cascade, $reason, User $user, $tags = null
2424  ) {
2425  global $wgCascadingRestrictionLevels, $wgContLang;
2426 
2427  if ( wfReadOnly() ) {
2428  return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
2429  }
2430 
2431  $this->loadPageData( 'fromdbmaster' );
2432  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2433  $id = $this->getId();
2434 
2435  if ( !$cascade ) {
2436  $cascade = false;
2437  }
2438 
2439  // Take this opportunity to purge out expired restrictions
2441 
2442  // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
2443  // we expect a single selection, but the schema allows otherwise.
2444  $isProtected = false;
2445  $protect = false;
2446  $changed = false;
2447 
2448  $dbw = wfGetDB( DB_MASTER );
2449 
2450  foreach ( $restrictionTypes as $action ) {
2451  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2452  $expiry[$action] = 'infinity';
2453  }
2454  if ( !isset( $limit[$action] ) ) {
2455  $limit[$action] = '';
2456  } elseif ( $limit[$action] != '' ) {
2457  $protect = true;
2458  }
2459 
2460  // Get current restrictions on $action
2461  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2462  if ( $current != '' ) {
2463  $isProtected = true;
2464  }
2465 
2466  if ( $limit[$action] != $current ) {
2467  $changed = true;
2468  } elseif ( $limit[$action] != '' ) {
2469  // Only check expiry change if the action is actually being
2470  // protected, since expiry does nothing on an not-protected
2471  // action.
2472  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2473  $changed = true;
2474  }
2475  }
2476  }
2477 
2478  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2479  $changed = true;
2480  }
2481 
2482  // If nothing has changed, do nothing
2483  if ( !$changed ) {
2484  return Status::newGood();
2485  }
2486 
2487  if ( !$protect ) { // No protection at all means unprotection
2488  $revCommentMsg = 'unprotectedarticle';
2489  $logAction = 'unprotect';
2490  } elseif ( $isProtected ) {
2491  $revCommentMsg = 'modifiedarticleprotection';
2492  $logAction = 'modify';
2493  } else {
2494  $revCommentMsg = 'protectedarticle';
2495  $logAction = 'protect';
2496  }
2497 
2498  // Truncate for whole multibyte characters
2499  $reason = $wgContLang->truncate( $reason, 255 );
2500 
2501  $logRelationsValues = [];
2502  $logRelationsField = null;
2503  $logParamsDetails = [];
2504 
2505  // Null revision (used for change tag insertion)
2506  $nullRevision = null;
2507 
2508  if ( $id ) { // Protection of existing page
2509  if ( !Hooks::run( 'ArticleProtect', [ &$this, &$user, $limit, $reason ] ) ) {
2510  return Status::newGood();
2511  }
2512 
2513  // Only certain restrictions can cascade...
2514  $editrestriction = isset( $limit['edit'] )
2515  ? [ $limit['edit'] ]
2516  : $this->mTitle->getRestrictions( 'edit' );
2517  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2518  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2519  }
2520  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2521  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2522  }
2523 
2524  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2525  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2526  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2527  }
2528  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2529  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2530  }
2531 
2532  // The schema allows multiple restrictions
2533  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2534  $cascade = false;
2535  }
2536 
2537  // insert null revision to identify the page protection change as edit summary
2538  $latest = $this->getLatest();
2539  $nullRevision = $this->insertProtectNullRevision(
2540  $revCommentMsg,
2541  $limit,
2542  $expiry,
2543  $cascade,
2544  $reason,
2545  $user
2546  );
2547 
2548  if ( $nullRevision === null ) {
2549  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2550  }
2551 
2552  $logRelationsField = 'pr_id';
2553 
2554  // Update restrictions table
2555  foreach ( $limit as $action => $restrictions ) {
2556  $dbw->delete(
2557  'page_restrictions',
2558  [
2559  'pr_page' => $id,
2560  'pr_type' => $action
2561  ],
2562  __METHOD__
2563  );
2564  if ( $restrictions != '' ) {
2565  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2566  $dbw->insert(
2567  'page_restrictions',
2568  [
2569  'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
2570  'pr_page' => $id,
2571  'pr_type' => $action,
2572  'pr_level' => $restrictions,
2573  'pr_cascade' => $cascadeValue,
2574  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2575  ],
2576  __METHOD__
2577  );
2578  $logRelationsValues[] = $dbw->insertId();
2579  $logParamsDetails[] = [
2580  'type' => $action,
2581  'level' => $restrictions,
2582  'expiry' => $expiry[$action],
2583  'cascade' => (bool)$cascadeValue,
2584  ];
2585  }
2586  }
2587 
2588  // Clear out legacy restriction fields
2589  $dbw->update(
2590  'page',
2591  [ 'page_restrictions' => '' ],
2592  [ 'page_id' => $id ],
2593  __METHOD__
2594  );
2595 
2596  Hooks::run( 'NewRevisionFromEditComplete',
2597  [ $this, $nullRevision, $latest, $user ] );
2598  Hooks::run( 'ArticleProtectComplete', [ &$this, &$user, $limit, $reason ] );
2599  } else { // Protection of non-existing page (also known as "title protection")
2600  // Cascade protection is meaningless in this case
2601  $cascade = false;
2602 
2603  if ( $limit['create'] != '' ) {
2604  $dbw->replace( 'protected_titles',
2605  [ [ 'pt_namespace', 'pt_title' ] ],
2606  [
2607  'pt_namespace' => $this->mTitle->getNamespace(),
2608  'pt_title' => $this->mTitle->getDBkey(),
2609  'pt_create_perm' => $limit['create'],
2610  'pt_timestamp' => $dbw->timestamp(),
2611  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2612  'pt_user' => $user->getId(),
2613  'pt_reason' => $reason,
2614  ], __METHOD__
2615  );
2616  $logParamsDetails[] = [
2617  'type' => 'create',
2618  'level' => $limit['create'],
2619  'expiry' => $expiry['create'],
2620  ];
2621  } else {
2622  $dbw->delete( 'protected_titles',
2623  [
2624  'pt_namespace' => $this->mTitle->getNamespace(),
2625  'pt_title' => $this->mTitle->getDBkey()
2626  ], __METHOD__
2627  );
2628  }
2629  }
2630 
2631  $this->mTitle->flushRestrictions();
2632  InfoAction::invalidateCache( $this->mTitle );
2633 
2634  if ( $logAction == 'unprotect' ) {
2635  $params = [];
2636  } else {
2637  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2638  $params = [
2639  '4::description' => $protectDescriptionLog, // parameter for IRC
2640  '5:bool:cascade' => $cascade,
2641  'details' => $logParamsDetails, // parameter for localize and api
2642  ];
2643  }
2644 
2645  // Update the protection log
2646  $logEntry = new ManualLogEntry( 'protect', $logAction );
2647  $logEntry->setTarget( $this->mTitle );
2648  $logEntry->setComment( $reason );
2649  $logEntry->setPerformer( $user );
2650  $logEntry->setParameters( $params );
2651  if ( !is_null( $nullRevision ) ) {
2652  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2653  }
2654  $logEntry->setTags( $tags );
2655  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2656  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2657  }
2658  $logId = $logEntry->insert();
2659  $logEntry->publish( $logId );
2660 
2661  return Status::newGood( $logId );
2662  }
2663 
2675  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2676  array $expiry, $cascade, $reason, $user = null
2677  ) {
2679  $dbw = wfGetDB( DB_MASTER );
2680 
2681  // Prepare a null revision to be added to the history
2682  $editComment = $wgContLang->ucfirst(
2683  wfMessage(
2684  $revCommentMsg,
2685  $this->mTitle->getPrefixedText()
2686  )->inContentLanguage()->text()
2687  );
2688  if ( $reason ) {
2689  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2690  }
2691  $protectDescription = $this->protectDescription( $limit, $expiry );
2692  if ( $protectDescription ) {
2693  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2694  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2695  ->inContentLanguage()->text();
2696  }
2697  if ( $cascade ) {
2698  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2699  $editComment .= wfMessage( 'brackets' )->params(
2700  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2701  )->inContentLanguage()->text();
2702  }
2703 
2704  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2705  if ( $nullRev ) {
2706  $nullRev->insertOn( $dbw );
2707 
2708  // Update page record and touch page
2709  $oldLatest = $nullRev->getParentId();
2710  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2711  }
2712 
2713  return $nullRev;
2714  }
2715 
2720  protected function formatExpiry( $expiry ) {
2722 
2723  if ( $expiry != 'infinity' ) {
2724  return wfMessage(
2725  'protect-expiring',
2726  $wgContLang->timeanddate( $expiry, false, false ),
2727  $wgContLang->date( $expiry, false, false ),
2728  $wgContLang->time( $expiry, false, false )
2729  )->inContentLanguage()->text();
2730  } else {
2731  return wfMessage( 'protect-expiry-indefinite' )
2732  ->inContentLanguage()->text();
2733  }
2734  }
2735 
2743  public function protectDescription( array $limit, array $expiry ) {
2744  $protectDescription = '';
2745 
2746  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2747  # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2748  # All possible message keys are listed here for easier grepping:
2749  # * restriction-create
2750  # * restriction-edit
2751  # * restriction-move
2752  # * restriction-upload
2753  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2754  # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2755  # with '' filtered out. All possible message keys are listed below:
2756  # * protect-level-autoconfirmed
2757  # * protect-level-sysop
2758  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2759  ->inContentLanguage()->text();
2760 
2761  $expiryText = $this->formatExpiry( $expiry[$action] );
2762 
2763  if ( $protectDescription !== '' ) {
2764  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2765  }
2766  $protectDescription .= wfMessage( 'protect-summary-desc' )
2767  ->params( $actionText, $restrictionsText, $expiryText )
2768  ->inContentLanguage()->text();
2769  }
2770 
2771  return $protectDescription;
2772  }
2773 
2785  public function protectDescriptionLog( array $limit, array $expiry ) {
2787 
2788  $protectDescriptionLog = '';
2789 
2790  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2791  $expiryText = $this->formatExpiry( $expiry[$action] );
2792  $protectDescriptionLog .= $wgContLang->getDirMark() .
2793  "[$action=$restrictions] ($expiryText)";
2794  }
2795 
2796  return trim( $protectDescriptionLog );
2797  }
2798 
2808  protected static function flattenRestrictions( $limit ) {
2809  if ( !is_array( $limit ) ) {
2810  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2811  }
2812 
2813  $bits = [];
2814  ksort( $limit );
2815 
2816  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2817  $bits[] = "$action=$restrictions";
2818  }
2819 
2820  return implode( ':', $bits );
2821  }
2822 
2839  public function doDeleteArticle(
2840  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2841  ) {
2842  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
2843  return $status->isGood();
2844  }
2845 
2864  public function doDeleteArticleReal(
2865  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2866  $tags = [], $logsubtype = 'delete'
2867  ) {
2868  global $wgUser, $wgContentHandlerUseDB;
2869 
2870  wfDebug( __METHOD__ . "\n" );
2871 
2873 
2874  if ( $this->mTitle->getDBkey() === '' ) {
2875  $status->error( 'cannotdelete',
2876  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2877  return $status;
2878  }
2879 
2880  $user = is_null( $user ) ? $wgUser : $user;
2881  if ( !Hooks::run( 'ArticleDelete',
2882  [ &$this, &$user, &$reason, &$error, &$status, $suppress ]
2883  ) ) {
2884  if ( $status->isOK() ) {
2885  // Hook aborted but didn't set a fatal status
2886  $status->fatal( 'delete-hook-aborted' );
2887  }
2888  return $status;
2889  }
2890 
2891  $dbw = wfGetDB( DB_MASTER );
2892  $dbw->startAtomic( __METHOD__ );
2893 
2894  $this->loadPageData( self::READ_LATEST );
2895  $id = $this->getId();
2896  // T98706: lock the page from various other updates but avoid using
2897  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2898  // the revisions queries (which also JOIN on user). Only lock the page
2899  // row and CAS check on page_latest to see if the trx snapshot matches.
2900  $lockedLatest = $this->lockAndGetLatest();
2901  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2902  $dbw->endAtomic( __METHOD__ );
2903  // Page not there or trx snapshot is stale
2904  $status->error( 'cannotdelete',
2905  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2906  return $status;
2907  }
2908 
2909  // Given the lock above, we can be confident in the title and page ID values
2910  $namespace = $this->getTitle()->getNamespace();
2911  $dbKey = $this->getTitle()->getDBkey();
2912 
2913  // At this point we are now comitted to returning an OK
2914  // status unless some DB query error or other exception comes up.
2915  // This way callers don't have to call rollback() if $status is bad
2916  // unless they actually try to catch exceptions (which is rare).
2917 
2918  // we need to remember the old content so we can use it to generate all deletion updates.
2919  $revision = $this->getRevision();
2920  try {
2921  $content = $this->getContent( Revision::RAW );
2922  } catch ( Exception $ex ) {
2923  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2924  . $ex->getMessage() );
2925 
2926  $content = null;
2927  }
2928 
2929  // Bitfields to further suppress the content
2930  if ( $suppress ) {
2931  $bitfield = 0;
2932  // This should be 15...
2933  $bitfield |= Revision::DELETED_TEXT;
2934  $bitfield |= Revision::DELETED_COMMENT;
2935  $bitfield |= Revision::DELETED_USER;
2936  $bitfield |= Revision::DELETED_RESTRICTED;
2937  $deletionFields = [ $dbw->addQuotes( $bitfield ) . ' AS deleted' ];
2938  } else {
2939  $deletionFields = [ 'rev_deleted AS deleted' ];
2940  }
2941 
2942  // For now, shunt the revision data into the archive table.
2943  // Text is *not* removed from the text table; bulk storage
2944  // is left intact to avoid breaking block-compression or
2945  // immutable storage schemes.
2946  // In the future, we may keep revisions and mark them with
2947  // the rev_deleted field, which is reserved for this purpose.
2948 
2949  // Get all of the page revisions
2950  $fields = array_diff( Revision::selectFields(), [ 'rev_deleted' ] );
2951  $res = $dbw->select(
2952  'revision',
2953  array_merge( $fields, $deletionFields ),
2954  [ 'rev_page' => $id ],
2955  __METHOD__,
2956  'FOR UPDATE'
2957  );
2958  // Build their equivalent archive rows
2959  $rowsInsert = [];
2960  foreach ( $res as $row ) {
2961  $rowInsert = [
2962  'ar_namespace' => $namespace,
2963  'ar_title' => $dbKey,
2964  'ar_comment' => $row->rev_comment,
2965  'ar_user' => $row->rev_user,
2966  'ar_user_text' => $row->rev_user_text,
2967  'ar_timestamp' => $row->rev_timestamp,
2968  'ar_minor_edit' => $row->rev_minor_edit,
2969  'ar_rev_id' => $row->rev_id,
2970  'ar_parent_id' => $row->rev_parent_id,
2971  'ar_text_id' => $row->rev_text_id,
2972  'ar_text' => '',
2973  'ar_flags' => '',
2974  'ar_len' => $row->rev_len,
2975  'ar_page_id' => $id,
2976  'ar_deleted' => $row->deleted,
2977  'ar_sha1' => $row->rev_sha1,
2978  ];
2979  if ( $wgContentHandlerUseDB ) {
2980  $rowInsert['ar_content_model'] = $row->rev_content_model;
2981  $rowInsert['ar_content_format'] = $row->rev_content_format;
2982  }
2983  $rowsInsert[] = $rowInsert;
2984  }
2985  // Copy them into the archive table
2986  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2987  // Save this so we can pass it to the ArticleDeleteComplete hook.
2988  $archivedRevisionCount = $dbw->affectedRows();
2989 
2990  // Clone the title and wikiPage, so we have the information we need when
2991  // we log and run the ArticleDeleteComplete hook.
2992  $logTitle = clone $this->mTitle;
2993  $wikiPageBeforeDelete = clone $this;
2994 
2995  // Now that it's safely backed up, delete it
2996  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2997  $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
2998 
2999  // Log the deletion, if the page was suppressed, put it in the suppression log instead
3000  $logtype = $suppress ? 'suppress' : 'delete';
3001 
3002  $logEntry = new ManualLogEntry( $logtype, $logsubtype );
3003  $logEntry->setPerformer( $user );
3004  $logEntry->setTarget( $logTitle );
3005  $logEntry->setComment( $reason );
3006  $logEntry->setTags( $tags );
3007  $logid = $logEntry->insert();
3008 
3009  $dbw->onTransactionPreCommitOrIdle(
3010  function () use ( $dbw, $logEntry, $logid ) {
3011  // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
3012  $logEntry->publish( $logid );
3013  },
3014  __METHOD__
3015  );
3016 
3017  $dbw->endAtomic( __METHOD__ );
3018 
3019  $this->doDeleteUpdates( $id, $content, $revision );
3020 
3021  Hooks::run( 'ArticleDeleteComplete', [
3022  &$wikiPageBeforeDelete,
3023  &$user,
3024  $reason,
3025  $id,
3026  $content,
3027  $logEntry,
3028  $archivedRevisionCount
3029  ] );
3030  $status->value = $logid;
3031 
3032  // Show log excerpt on 404 pages rather than just a link
3034  $key = wfMemcKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
3035  $cache->set( $key, 1, $cache::TTL_DAY );
3036 
3037  return $status;
3038  }
3039 
3046  public function lockAndGetLatest() {
3047  return (int)wfGetDB( DB_MASTER )->selectField(
3048  'page',
3049  'page_latest',
3050  [
3051  'page_id' => $this->getId(),
3052  // Typically page_id is enough, but some code might try to do
3053  // updates assuming the title is the same, so verify that
3054  'page_namespace' => $this->getTitle()->getNamespace(),
3055  'page_title' => $this->getTitle()->getDBkey()
3056  ],
3057  __METHOD__,
3058  [ 'FOR UPDATE' ]
3059  );
3060  }
3061 
3071  public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null ) {
3072  try {
3073  $countable = $this->isCountable();
3074  } catch ( Exception $ex ) {
3075  // fallback for deleting broken pages for which we cannot load the content for
3076  // some reason. Note that doDeleteArticleReal() already logged this problem.
3077  $countable = false;
3078  }
3079 
3080  // Update site status
3081  DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
3082 
3083  // Delete pagelinks, update secondary indexes, etc
3084  $updates = $this->getDeletionUpdates( $content );
3085  foreach ( $updates as $update ) {
3086  DeferredUpdates::addUpdate( $update );
3087  }
3088 
3089  // Reparse any pages transcluding this page
3090  LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
3091 
3092  // Reparse any pages including this image
3093  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3094  LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
3095  }
3096 
3097  // Clear caches
3098  WikiPage::onArticleDelete( $this->mTitle );
3100  $this->mTitle, $revision, null, wfWikiID()
3101  );
3102 
3103  // Reset this object and the Title object
3104  $this->loadFromRow( false, self::READ_LATEST );
3105 
3106  // Search engine
3107  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3108  }
3109 
3139  public function doRollback(
3140  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3141  ) {
3142  $resultDetails = null;
3143 
3144  // Check permissions
3145  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3146  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3147  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3148 
3149  if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3150  $errors[] = [ 'sessionfailure' ];
3151  }
3152 
3153  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3154  $errors[] = [ 'actionthrottledtext' ];
3155  }
3156 
3157  // If there were errors, bail out now
3158  if ( !empty( $errors ) ) {
3159  return $errors;
3160  }
3161 
3162  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3163  }
3164 
3185  public function commitRollback( $fromP, $summary, $bot,
3186  &$resultDetails, User $guser, $tags = null
3187  ) {
3188  global $wgUseRCPatrol, $wgContLang;
3189 
3190  $dbw = wfGetDB( DB_MASTER );
3191 
3192  if ( wfReadOnly() ) {
3193  return [ [ 'readonlytext' ] ];
3194  }
3195 
3196  // Get the last editor
3197  $current = $this->getRevision();
3198  if ( is_null( $current ) ) {
3199  // Something wrong... no page?
3200  return [ [ 'notanarticle' ] ];
3201  }
3202 
3203  $from = str_replace( '_', ' ', $fromP );
3204  // User name given should match up with the top revision.
3205  // If the user was deleted then $from should be empty.
3206  if ( $from != $current->getUserText() ) {
3207  $resultDetails = [ 'current' => $current ];
3208  return [ [ 'alreadyrolled',
3209  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3210  htmlspecialchars( $fromP ),
3211  htmlspecialchars( $current->getUserText() )
3212  ] ];
3213  }
3214 
3215  // Get the last edit not by this person...
3216  // Note: these may not be public values
3217  $user = intval( $current->getUser( Revision::RAW ) );
3218  $user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
3219  $s = $dbw->selectRow( 'revision',
3220  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3221  [ 'rev_page' => $current->getPage(),
3222  "rev_user != {$user} OR rev_user_text != {$user_text}"
3223  ], __METHOD__,
3224  [ 'USE INDEX' => 'page_timestamp',
3225  'ORDER BY' => 'rev_timestamp DESC' ]
3226  );
3227  if ( $s === false ) {
3228  // No one else ever edited this page
3229  return [ [ 'cantrollback' ] ];
3230  } elseif ( $s->rev_deleted & Revision::DELETED_TEXT
3231  || $s->rev_deleted & Revision::DELETED_USER
3232  ) {
3233  // Only admins can see this text
3234  return [ [ 'notvisiblerev' ] ];
3235  }
3236 
3237  // Generate the edit summary if necessary
3238  $target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST );
3239  if ( empty( $summary ) ) {
3240  if ( $from == '' ) { // no public user name
3241  $summary = wfMessage( 'revertpage-nouser' );
3242  } else {
3243  $summary = wfMessage( 'revertpage' );
3244  }
3245  }
3246 
3247  // Allow the custom summary to use the same args as the default message
3248  $args = [
3249  $target->getUserText(), $from, $s->rev_id,
3250  $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3251  $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
3252  ];
3253  if ( $summary instanceof Message ) {
3254  $summary = $summary->params( $args )->inContentLanguage()->text();
3255  } else {
3257  }
3258 
3259  // Trim spaces on user supplied text
3260  $summary = trim( $summary );
3261 
3262  // Truncate for whole multibyte characters.
3263  $summary = $wgContLang->truncate( $summary, 255 );
3264 
3265  // Save
3267 
3268  if ( $guser->isAllowed( 'minoredit' ) ) {
3269  $flags |= EDIT_MINOR;
3270  }
3271 
3272  if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3274  }
3275 
3276  $targetContent = $target->getContent();
3277  $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
3278 
3279  // Actually store the edit
3280  $status = $this->doEditContent(
3281  $targetContent,
3282  $summary,
3283  $flags,
3284  $target->getId(),
3285  $guser,
3286  null,
3287  $tags
3288  );
3289 
3290  // Set patrolling and bot flag on the edits, which gets rollbacked.
3291  // This is done even on edit failure to have patrolling in that case (bug 62157).
3292  $set = [];
3293  if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3294  // Mark all reverted edits as bot
3295  $set['rc_bot'] = 1;
3296  }
3297 
3298  if ( $wgUseRCPatrol ) {
3299  // Mark all reverted edits as patrolled
3300  $set['rc_patrolled'] = 1;
3301  }
3302 
3303  if ( count( $set ) ) {
3304  $dbw->update( 'recentchanges', $set,
3305  [ /* WHERE */
3306  'rc_cur_id' => $current->getPage(),
3307  'rc_user_text' => $current->getUserText(),
3308  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3309  ],
3310  __METHOD__
3311  );
3312  }
3313 
3314  if ( !$status->isOK() ) {
3315  return $status->getErrorsArray();
3316  }
3317 
3318  // raise error, when the edit is an edit without a new version
3319  $statusRev = isset( $status->value['revision'] )
3320  ? $status->value['revision']
3321  : null;
3322  if ( !( $statusRev instanceof Revision ) ) {
3323  $resultDetails = [ 'current' => $current ];
3324  return [ [ 'alreadyrolled',
3325  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3326  htmlspecialchars( $fromP ),
3327  htmlspecialchars( $current->getUserText() )
3328  ] ];
3329  }
3330 
3331  if ( $changingContentModel ) {
3332  // If the content model changed during the rollback,
3333  // make sure it gets logged to Special:Log/contentmodel
3334  $log = new ManualLogEntry( 'contentmodel', 'change' );
3335  $log->setPerformer( $guser );
3336  $log->setTarget( $this->mTitle );
3337  $log->setComment( $summary );
3338  $log->setParameters( [
3339  '4::oldmodel' => $current->getContentModel(),
3340  '5::newmodel' => $targetContent->getModel(),
3341  ] );
3342 
3343  $logId = $log->insert( $dbw );
3344  $log->publish( $logId );
3345  }
3346 
3347  $revId = $statusRev->getId();
3348 
3349  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3350 
3351  $resultDetails = [
3352  'summary' => $summary,
3353  'current' => $current,
3354  'target' => $target,
3355  'newid' => $revId
3356  ];
3357 
3358  return [];
3359  }
3360 
3372  public static function onArticleCreate( Title $title ) {
3373  // Update existence markers on article/talk tabs...
3374  $other = $title->getOtherPage();
3375 
3376  $other->purgeSquid();
3377 
3378  $title->touchLinks();
3379  $title->purgeSquid();
3380  $title->deleteTitleProtection();
3381 
3382  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3383 
3384  if ( $title->getNamespace() == NS_CATEGORY ) {
3385  // Load the Category object, which will schedule a job to create
3386  // the category table row if necessary. Checking a replica DB is ok
3387  // here, in the worst case it'll run an unnecessary recount job on
3388  // a category that probably doesn't have many members.
3389  Category::newFromTitle( $title )->getID();
3390  }
3391  }
3392 
3398  public static function onArticleDelete( Title $title ) {
3400 
3401  // Update existence markers on article/talk tabs...
3402  $other = $title->getOtherPage();
3403 
3404  $other->purgeSquid();
3405 
3406  $title->touchLinks();
3407  $title->purgeSquid();
3408 
3409  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3410 
3411  // File cache
3413  InfoAction::invalidateCache( $title );
3414 
3415  // Messages
3416  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3417  MessageCache::singleton()->replace( $title->getDBkey(), false );
3418 
3419  if ( $wgContLang->hasVariants() ) {
3420  $wgContLang->updateConversionTable( $title );
3421  }
3422  }
3423 
3424  // Images
3425  if ( $title->getNamespace() == NS_FILE ) {
3426  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
3427  }
3428 
3429  // User talk pages
3430  if ( $title->getNamespace() == NS_USER_TALK ) {
3431  $user = User::newFromName( $title->getText(), false );
3432  if ( $user ) {
3433  $user->setNewtalk( false );
3434  }
3435  }
3436 
3437  // Image redirects
3438  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3439  }
3440 
3447  public static function onArticleEdit( Title $title, Revision $revision = null ) {
3448  // Invalidate caches of articles which include this page
3449  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
3450 
3451  // Invalidate the caches of all pages which redirect here
3452  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
3453 
3454  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3455 
3456  // Purge CDN for this page only
3457  $title->purgeSquid();
3458  // Clear file cache for this page only
3460 
3461  $revid = $revision ? $revision->getId() : null;
3462  DeferredUpdates::addCallableUpdate( function() use ( $title, $revid ) {
3463  InfoAction::invalidateCache( $title, $revid );
3464  } );
3465  }
3466 
3475  public function getCategories() {
3476  $id = $this->getId();
3477  if ( $id == 0 ) {
3478  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3479  }
3480 
3481  $dbr = wfGetDB( DB_REPLICA );
3482  $res = $dbr->select( 'categorylinks',
3483  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3484  // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3485  // as not being aliases, and NS_CATEGORY is numeric
3486  [ 'cl_from' => $id ],
3487  __METHOD__ );
3488 
3489  return TitleArray::newFromResult( $res );
3490  }
3491 
3498  public function getHiddenCategories() {
3499  $result = [];
3500  $id = $this->getId();
3501 
3502  if ( $id == 0 ) {
3503  return [];
3504  }
3505 
3506  $dbr = wfGetDB( DB_REPLICA );
3507  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3508  [ 'cl_to' ],
3509  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3510  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3511  __METHOD__ );
3512 
3513  if ( $res !== false ) {
3514  foreach ( $res as $row ) {
3515  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3516  }
3517  }
3518 
3519  return $result;
3520  }
3521 
3531  public static function getAutosummary( $oldtext, $newtext, $flags ) {
3532  // NOTE: stub for backwards-compatibility. assumes the given text is
3533  // wikitext. will break horribly if it isn't.
3534 
3535  wfDeprecated( __METHOD__, '1.21' );
3536 
3538  $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
3539  $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
3540 
3541  return $handler->getAutosummary( $oldContent, $newContent, $flags );
3542  }
3543 
3551  public function getAutoDeleteReason( &$hasHistory ) {
3552  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3553  }
3554 
3563  public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3564  $id = $id ?: $this->getId();
3565  $dbw = wfGetDB( DB_MASTER );
3566  $method = __METHOD__;
3567  // Do this at the end of the commit to reduce lock wait timeouts
3568  $dbw->onTransactionPreCommitOrIdle(
3569  function () use ( $dbw, $added, $deleted, $id, $method ) {
3570  $ns = $this->getTitle()->getNamespace();
3571 
3572  $addFields = [ 'cat_pages = cat_pages + 1' ];
3573  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3574  if ( $ns == NS_CATEGORY ) {
3575  $addFields[] = 'cat_subcats = cat_subcats + 1';
3576  $removeFields[] = 'cat_subcats = cat_subcats - 1';
3577  } elseif ( $ns == NS_FILE ) {
3578  $addFields[] = 'cat_files = cat_files + 1';
3579  $removeFields[] = 'cat_files = cat_files - 1';
3580  }
3581 
3582  if ( count( $added ) ) {
3583  $existingAdded = $dbw->selectFieldValues(
3584  'category',
3585  'cat_title',
3586  [ 'cat_title' => $added ],
3587  $method
3588  );
3589 
3590  // For category rows that already exist, do a plain
3591  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3592  // to avoid creating gaps in the cat_id sequence.
3593  if ( count( $existingAdded ) ) {
3594  $dbw->update(
3595  'category',
3596  $addFields,
3597  [ 'cat_title' => $existingAdded ],
3598  $method
3599  );
3600  }
3601 
3602  $missingAdded = array_diff( $added, $existingAdded );
3603  if ( count( $missingAdded ) ) {
3604  $insertRows = [];
3605  foreach ( $missingAdded as $cat ) {
3606  $insertRows[] = [
3607  'cat_title' => $cat,
3608  'cat_pages' => 1,
3609  'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
3610  'cat_files' => ( $ns == NS_FILE ) ? 1 : 0,
3611  ];
3612  }
3613  $dbw->upsert(
3614  'category',
3615  $insertRows,
3616  [ 'cat_title' ],
3617  $addFields,
3618  $method
3619  );
3620  }
3621  }
3622 
3623  if ( count( $deleted ) ) {
3624  $dbw->update(
3625  'category',
3626  $removeFields,
3627  [ 'cat_title' => $deleted ],
3628  $method
3629  );
3630  }
3631 
3632  foreach ( $added as $catName ) {
3633  $cat = Category::newFromName( $catName );
3634  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3635  }
3636 
3637  foreach ( $deleted as $catName ) {
3638  $cat = Category::newFromName( $catName );
3639  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3640  }
3641 
3642  // Refresh counts on categories that should be empty now, to
3643  // trigger possible deletion. Check master for the most
3644  // up-to-date cat_pages.
3645  if ( count( $deleted ) ) {
3646  $rows = $dbw->select(
3647  'category',
3648  [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
3649  [ 'cat_title' => $deleted, 'cat_pages <= 0' ],
3650  $method
3651  );
3652  foreach ( $rows as $row ) {
3653  $cat = Category::newFromRow( $row );
3654  $cat->refreshCounts();
3655  }
3656  }
3657  },
3658  __METHOD__
3659  );
3660  }
3661 
3669  if ( wfReadOnly() ) {
3670  return;
3671  }
3672 
3673  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3674  [ $this, $this->mTitle, $parserOutput ]
3675  ) ) {
3676  return;
3677  }
3678 
3679  $config = RequestContext::getMain()->getConfig();
3680 
3681  $params = [
3682  'isOpportunistic' => true,
3683  'rootJobTimestamp' => $parserOutput->getCacheTime()
3684  ];
3685 
3686  if ( $this->mTitle->areRestrictionsCascading() ) {
3687  // If the page is cascade protecting, the links should really be up-to-date
3688  JobQueueGroup::singleton()->lazyPush(
3689  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3690  );
3691  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3692  // Assume the output contains "dynamic" time/random based magic words.
3693  // Only update pages that expired due to dynamic content and NOT due to edits
3694  // to referenced templates/files. When the cache expires due to dynamic content,
3695  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3696  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3697  // template/file edit already triggered recursive RefreshLinksJob jobs.
3698  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3699  // If a page is uncacheable, do not keep spamming a job for it.
3700  // Although it would be de-duplicated, it would still waste I/O.
3702  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3703  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3704  if ( $cache->add( $key, time(), $ttl ) ) {
3705  JobQueueGroup::singleton()->lazyPush(
3706  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3707  );
3708  }
3709  }
3710  }
3711  }
3712 
3722  public function getDeletionUpdates( Content $content = null ) {
3723  if ( !$content ) {
3724  // load content object, which may be used to determine the necessary updates.
3725  // XXX: the content may not be needed to determine the updates.
3726  try {
3727  $content = $this->getContent( Revision::RAW );
3728  } catch ( Exception $ex ) {
3729  // If we can't load the content, something is wrong. Perhaps that's why
3730  // the user is trying to delete the page, so let's not fail in that case.
3731  // Note that doDeleteArticleReal() will already have logged an issue with
3732  // loading the content.
3733  }
3734  }
3735 
3736  if ( !$content ) {
3737  $updates = [];
3738  } else {
3739  $updates = $content->getDeletionUpdates( $this );
3740  }
3741 
3742  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3743  return $updates;
3744  }
3745 
3753  public function isLocal() {
3754  return true;
3755  }
3756 }
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:525
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:550
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:827
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:115
static newFromRow($row)
Make a Title object from a DB row.
Definition: Title.php:450
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3023
setLastEdit(Revision $revision)
Set the latest revision.
Definition: WikiPage.php:650
makeParserOptions($context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2060
const PURGE_ALL
Definition: WikiPage.php:89
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:3372
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:1403
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4430
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1359
static getMainWANInstance()
Get the main WAN cache object.
string $mLinksUpdated
Definition: WikiPage.php:84
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
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
Database error base class.
Definition: DBError.php:26
static newFromName($name)
Factory function.
Definition: Category.php:120
the array() calling protocol came about after MediaWiki 1.4rc1.
replaceSectionContent($sectionId, Content $sectionContent, $sectionTitle= '', $edittime=null)
Definition: WikiPage.php:1436
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:239
getLatest()
Get the page_latest field.
Definition: WikiPage.php:561
stdClass $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:49
$context
Definition: load.php:50
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
string $mTouched
Definition: WikiPage.php:79
getUser($audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:740
doPurge($flags=self::PURGE_ALL)
Perform the actions of a page purging.
Definition: WikiPage.php:1138
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:880
clearNotification(&$title, $oldid=0)
Clear the user's notification timestamp for the given title.
Definition: User.php:3555
int $mId
Definition: WikiPage.php:54
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
static checkCache(Title $title, Content $content, User $user)
Check that a prepared edit is in cache and still up-to-date.
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:1936
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
int $mDataLoadedFrom
One of the READ_* constants.
Definition: WikiPage.php:59
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:3563
getTimestamp()
Definition: Revision.php:1194
Class the manages updates of *_link tables as well as similar extension-managed tables.
Definition: LinksUpdate.php:33
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3413
static getForModelID($modelId)
Returns the ContentHandler singleton for the given model ID.
protectDescription(array $limit, array $expiry)
Builds the description to serve as comment for the edit.
Definition: WikiPage.php:2743
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:2102
static newFatal($message)
Factory function for fatal errors.
Definition: StatusValue.php:63
Set options of the Parser.
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
Definition: WikiPage.php:910
pingLimiter($action= 'edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1794
Title $mTitle
Definition: WikiPage.php:38
updateRevisionOn($dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1260
const EDIT_INTERNAL
Definition: Defines.php:153
clear()
Clear the object.
Definition: WikiPage.php:240
Handles purging appropriate CDN URLs given a title (or titles)
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3668
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
$source
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:810
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context $revId
Definition: hooks.txt:1046
getOtherPage()
Get the other title for this page, if this is a subject page get the talk page, if it is a subject pa...
Definition: Title.php:1332
static getLocalClusterInstance()
Get the main cluster-local cache object.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2703
getContributors()
Get a list of users who have edited this article, not including the user who made the most recent rev...
Definition: WikiPage.php:1011
checkFlags($flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1518
const EDIT_MINOR
Definition: Defines.php:148
static getMainStashInstance()
Get the cache object for the main stash.
const EDIT_UPDATE
Definition: Defines.php:147
insertProtectNullRevision($revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2675
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:159
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
getTouched()
Get the page_touched field.
Definition: WikiPage.php:539
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, purges caches.
Definition: WikiPage.php:2864
matchEditToken($val, $salt= '', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4280
getDeletionUpdates(Content $content=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3722
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition: WikiPage.php:74
magic word & $parser
Definition: hooks.txt:2487
const DB_MASTER
Definition: defines.php:23
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:251
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration which are documented in DefaultSettings php There is no comprehensive documentation for the remaining however some of the most important ones are listed below They are typically initialised either in index php or in Setup php For a description of the see design txt $wgTitle Title object created from the request URL $wgOut OutputPage object for HTTP response $wgUser User object for the user associated with the current request $wgLang Language object selected by user preferences $wgContLang Language object associated with the wiki being viewed $wgParser Parser object Parser extensions register their hooks here $wgRequest WebRequest object
Definition: globals.txt:25
loadLastEdit()
Loads everything except the text This isn't necessary for all uses, so it's only done if needed...
Definition: WikiPage.php:610
const PURGE_CLUSTER_PCACHE
Definition: WikiPage.php:87
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2108
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist & $tables
Definition: hooks.txt:1007
pageDataFromTitle($dbr, $title, $options=[])
Fetch a page record matching the Title object's namespace and title using a sanitized title string...
Definition: WikiPage.php:337
getActionOverrides()
Definition: WikiPage.php:211
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:481
static queueRecursiveJobsForTable(Title $title, $table)
Queue a RefreshLinks job for any table.
wfArrayDiff2($a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
wfDebug($text, $dest= 'all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
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:Associative array mapping language codes to prefixed links of the form"language:title".&$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:1934
getTitleKey()
Get the user's name escaped by underscores.
Definition: User.php:2144
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.
if($line===false) $args
Definition: cdb.php:64
doEdit($text, $summary, $flags=0, $baseRevId=false, $user=null)
Change an existing article or create a new article.
Definition: WikiPage.php:1584
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:499
getLastPurgeTimestamp()
Get the last time a user explicitly purged the page via action=purge.
Definition: WikiPage.php:1196
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:528
Database independant search index updater.
getSize()
Returns the content's nominal size in "bogo-bytes".
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
$mIsRedirect
Definition: WikiPage.php:44
wfMsgReplaceArgs($message, $args)
Replace message parameter keys on the given formatted output.
loadFromRow($data, $from)
Load the object from a database row.
Definition: WikiPage.php:407
static getLocalizedName($name, Language $lang=null)
Returns the localized name for a given content model.
getRevision()
Get the latest revision.
Definition: WikiPage.php:659
getContent($audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:680
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:24
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:69
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:110
wfGetLB($wiki=false)
Get a load balancer object.
wfEscapeWikiText($text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
wfReadOnly()
Check whether the wiki is in read-only mode.
deleteTitleProtection()
Remove any title protection due to page existing.
Definition: Title.php:2600
static getMain()
Static methods.
static singleton()
Get an instance of this class.
Definition: LinkCache.php:64
const FOR_PUBLIC
Definition: Revision.php:92
const EDIT_FORCE_BOT
Definition: Defines.php:150
static clearFileCache(Title $title)
Clear the file caches for a page for all actions.
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:3185
Revision $mLastRevision
Definition: WikiPage.php:69
isAllowed($action= '')
Internal mechanics of testing a permission.
Definition: User.php:3443
Class to invalidate the HTML cache of all the pages linking to a given title.
getDBkey()
Get the main part with underscores.
Definition: Title.php:898
wfWarn($msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
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 unsetoffset-wrap String Wrap the message in html(usually something like"&lt
getComment($audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:796
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context $parserOutput
Definition: hooks.txt:1046
getId()
Get revision ID.
Definition: Revision.php:729
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:1046
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:962
const NS_MEDIA
Definition: Defines.php:44
$res
Definition: database.txt:21
static loadFromTimestamp($db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:301
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:59
static getContentText(Content $content=null)
Convenience function for getting flat text from a Content object.
getContentHandler()
Returns the content handler appropriate for this revision's content model.
Definition: Revision.php:1175
$summary
doDeleteArticle($reason, $suppress=false, $u1=null, $u2=null, &$error= '', User $user=null)
Same as doDeleteArticleReal(), but returns a simple boolean.
Definition: WikiPage.php:2839
static newNullRevision($dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
Definition: Revision.php:1678
static newFromRow($row, $from= 'fromdb')
Constructor from a database row.
Definition: WikiPage.php:180
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:3139
Class for handling updates to the site_stats table.
incEditCount()
Deferred version of incEditCountImmediate()
Definition: User.php:4935
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition: WikiPage.php:103
doCreate(Content $content, $flags, User $user, $summary, array $meta)
Definition: WikiPage.php:1933
const EDIT_AUTOSUMMARY
Definition: Defines.php:152
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
doModify(Content $content, $flags, User $user, $summary, array $meta)
Definition: WikiPage.php:1761
Base interface for content objects.
Definition: Content.php:34
getCacheTime()
Definition: CacheTime.php:50
static newFromTitle($title)
Factory function.
Definition: Category.php:140
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:572
Deferrable Update for closure/callback updates via IDatabase::doAtomicSection()
$cache
Definition: mcc.php:33
$params
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3498
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1118
getTitle()
Get the title object of the article.
Definition: WikiPage.php:232
const NS_CATEGORY
Definition: Defines.php:70
const EDIT_SUPPRESS_RC
Definition: Defines.php:149
static selectFields()
Return the list of revision fields that should be selected to create a new revision.
Definition: Revision.php:442
wfDeprecated($function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
const TS_MW
MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
Definition: defines.php:11
static isIP($name)
Does the string match an anonymous IP address?
Definition: User.php:788
wfIncrStats($key, $count=1)
Increment a statistics counter.
const DELETED_RESTRICTED
Definition: Revision.php:88
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty & $sectionContent
Definition: hooks.txt:2491
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:953
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:1905
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for...
Definition: WikiPage.php:472
static newGood($value=null)
Factory function for good results.
Definition: StatusValue.php:76
static run($event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:131
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:921
static getDBOptions($bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
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
equals(Title $title)
Compare with another title.
Definition: Title.php:4226
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
const NS_FILE
Definition: Defines.php:62
static newFromResult($res)
Definition: TitleArray.php:38
static makeContent($text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
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:1721
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:808
const RAW
Definition: Revision.php:94
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
Special handling for file pages.
getRedirectURL($rt)
Get the Title object or URL to use for a redirect.
Definition: WikiPage.php:973
const NS_MEDIAWIKI
Definition: Defines.php:64
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:224
equals(Content $that=null)
Returns true if this Content objects is conceptually equivalent to the given Content object...
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 local account $user
Definition: hooks.txt:242
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3551
static onArticleEdit(Title $title, Revision $revision=null)
Purge caches on page update etc.
Definition: WikiPage.php:3447
const DELETED_TEXT
Definition: Revision.php:85
static singleton($wiki=false)
static singleton()
Get an instance of this object.
Definition: ParserCache.php:36
prepareSave(WikiPage $page, $flags, $parentRevId, User $user)
Prepare Content for saving.
getUserText($audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:778
Class representing a MediaWiki article and history.
Definition: WikiPage.php:32
static newFromId($id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:110
replaceSectionAtRev($sectionId, Content $sectionContent, $sectionTitle= '', $baseRevId=null)
Definition: WikiPage.php:1475
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:2103
$from
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
static newDynamic(Title $title, array $params)
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
wfRandom()
Get a random decimal value between 0 and 1, in a way not likely to give duplicate values for any real...
Job to add recent change entries mentioning category membership changes.
const DELETED_USER
Definition: Revision.php:87
Class for creating log entries manually, to inject them into the database.
Definition: LogEntry.php:394
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
getCategories()
#@-
Definition: WikiPage.php:3475
hasDynamicContent()
Check whether the cache TTL was lowered due to dynamic content.
doEditUpdates(Revision $revision, User $user, array $options=[])
Do standard deferred updates after page edit.
Definition: WikiPage.php:2245
getId()
Get the user's ID.
Definition: User.php:2083
insertOn($dbw)
Insert a new revision into the database, returning the new revision ID number on success and dies hor...
Definition: Revision.php:1396
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
isLocal()
Whether this content displayed on this page comes from the local database.
Definition: WikiPage.php:3753
const EDIT_NEW
Definition: Defines.php:146
getTimestamp()
Definition: WikiPage.php:713
insertOn($dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1216
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content $content
Definition: hooks.txt:1046
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3398
static convertSelectType($type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:192
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1418
static newPrioritized(Title $title, array $params)
preSaveTransform(Title $title, User $user, ParserOptions $parserOptions)
Returns a Content object with pre-save transformations applied (or this object if no transformations ...
getContent($audience=self::FOR_PUBLIC, User $user=null)
Fetch revision content if it's available to the specified audience.
Definition: Revision.php:1073
updateRedirectOn($dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1331
$mDataLoaded
Definition: WikiPage.php:43
static newFromRow($row)
Definition: Revision.php:230
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1065
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...
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:871
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object to manipulate or replace but no entry for that model exists in $wgContentHandlers if desired 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 inclusive $limit
Definition: hooks.txt:1046
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 local content language as $wgContLang
Definition: design.txt:56
Interface for database access objects.
updateIfNewerOn($dbw, $revision)
If the given revision is newer than the currently set page_latest, update the page record...
Definition: WikiPage.php:1366
static newFromID($id, $from= 'fromdb')
Constructor from a page id.
Definition: WikiPage.php:153
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1046
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
loadPageData($from= 'fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:367
insertRedirectEntry(Title $rt, $oldLatest=null)
Insert or update the redirect table entry for this page to indicate it redirects to $rt...
Definition: WikiPage.php:936
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1085
wfMemcKey()
Make a cache key for the local wiki.
static addCallableUpdate($callable, $stage=self::POSTSEND, IDatabase $dbw=null)
Add a callable update.
const DB_REPLICA
Definition: defines.php:22
setTimestamp($ts)
Set the page timestamp (use only to avoid DB queries)
Definition: WikiPage.php:727
const PURGE_CDN_CACHE
Definition: WikiPage.php:86
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:3046
wfLogWarning($msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
const DELETED_COMMENT
Definition: Revision.php:86
doEditContent(Content $content, $summary, $flags=0, $baseRevId=false, User $user=null, $serialFormat=null, $tags=[])
Change an existing article or create a new article.
Definition: WikiPage.php:1649
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...
pageData($dbr, $conditions, $options=[])
Fetch a page record with the given conditions.
Definition: WikiPage.php:316
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:802
getModel()
Returns the ID of the content model used by this Content object.
static logException($e)
Log an exception to the exception log (if enabled).
static newFromRow($row, $title=null)
Factory function, for constructing a Category object from a result set.
Definition: Category.php:173
prepareTextForEdit($text, $revid=null, User $user=null)
Prepare text which is about to be saved.
Definition: WikiPage.php:2082
wfTimestampOrNull($outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
static selectFields()
Return the list of revision fields that should be selected to create a new page.
Definition: WikiPage.php:281
static getAutosummary($oldtext, $newtext, $flags)
Return an applicable autosummary if one exists for the given edit.
Definition: WikiPage.php:3531
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:271
const NS_USER_TALK
Definition: Defines.php:59
formatExpiry($expiry)
Definition: WikiPage.php:2720
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 local account incomplete not yet checked for validity & $retval
Definition: hooks.txt:242
Title $mRedirectTarget
Definition: WikiPage.php:64
const PURGE_GLOBAL_PCACHE
Definition: WikiPage.php:88
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:95
doDeleteUpdates($id, Content $content=null, Revision $revision=null)
Do some database updates after deletion.
Definition: WikiPage.php:3071
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2491
static makeTitle($ns, $title, $fragment= '', $interwiki= '')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:511
getText($audience=Revision::FOR_PUBLIC, User $user=null)
Get the text of the current revision.
Definition: WikiPage.php:700
pageDataFromId($dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:351
Special handling for category pages.
static singleton()
Get the signleton instance of this class.
purgeSquid()
Purge all applicable CDN URLs.
Definition: Title.php:3593
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:2422
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:2808
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached $page
Definition: hooks.txt:2491
static runLegacyHooks($event, $args=[], $deprecatedVersion=null)
Call a legacy hook that uses text instead of Content objects.
$wgUser
Definition: Setup.php:806
getCreator($audience=Revision::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:759
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2785