MediaWiki  1.27.4
WikiPage.php
Go to the documentation of this file.
1 <?php
29 class WikiPage implements Page, IDBAccessObject {
30  // Constants for $mDataLoadedFrom and related
31 
35  public $mTitle = null;
36 
40  public $mDataLoaded = false; // !< Boolean
41  public $mIsRedirect = false; // !< Boolean
42  public $mLatest = false; // !< Integer (false means "not loaded")
46  public $mPreparedEdit = false;
47 
51  protected $mId = null;
52 
56  protected $mDataLoadedFrom = self::READ_NONE;
57 
61  protected $mRedirectTarget = null;
62 
66  protected $mLastRevision = null;
67 
71  protected $mTimestamp = '';
72 
76  protected $mTouched = '19700101000000';
77 
81  protected $mLinksUpdated = '19700101000000';
82 
87  public function __construct( Title $title ) {
88  $this->mTitle = $title;
89  }
90 
99  public static function factory( Title $title ) {
100  $ns = $title->getNamespace();
101 
102  if ( $ns == NS_MEDIA ) {
103  throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
104  } elseif ( $ns < 0 ) {
105  throw new MWException( "Invalid or virtual namespace $ns given." );
106  }
107 
108  switch ( $ns ) {
109  case NS_FILE:
110  $page = new WikiFilePage( $title );
111  break;
112  case NS_CATEGORY:
113  $page = new WikiCategoryPage( $title );
114  break;
115  default:
116  $page = new WikiPage( $title );
117  }
118 
119  return $page;
120  }
121 
132  public static function newFromID( $id, $from = 'fromdb' ) {
133  // page id's are never 0 or negative, see bug 61166
134  if ( $id < 1 ) {
135  return null;
136  }
137 
138  $from = self::convertSelectType( $from );
139  $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE );
140  $row = $db->selectRow(
141  'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
142  if ( !$row ) {
143  return null;
144  }
145  return self::newFromRow( $row, $from );
146  }
147 
159  public static function newFromRow( $row, $from = 'fromdb' ) {
160  $page = self::factory( Title::newFromRow( $row ) );
161  $page->loadFromRow( $row, $from );
162  return $page;
163  }
164 
171  private static function convertSelectType( $type ) {
172  switch ( $type ) {
173  case 'fromdb':
174  return self::READ_NORMAL;
175  case 'fromdbmaster':
176  return self::READ_LATEST;
177  case 'forupdate':
178  return self::READ_LOCKING;
179  default:
180  // It may already be an integer or whatever else
181  return $type;
182  }
183  }
184 
195  public function getActionOverrides() {
196  $content_handler = $this->getContentHandler();
197  return $content_handler->getActionOverrides();
198  }
199 
209  public function getContentHandler() {
211  }
212 
217  public function getTitle() {
218  return $this->mTitle;
219  }
220 
225  public function clear() {
226  $this->mDataLoaded = false;
227  $this->mDataLoadedFrom = self::READ_NONE;
228 
229  $this->clearCacheFields();
230  }
231 
236  protected function clearCacheFields() {
237  $this->mId = null;
238  $this->mRedirectTarget = null; // Title object if set
239  $this->mLastRevision = null; // Latest revision
240  $this->mTouched = '19700101000000';
241  $this->mLinksUpdated = '19700101000000';
242  $this->mTimestamp = '';
243  $this->mIsRedirect = false;
244  $this->mLatest = false;
245  // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
246  // the requested rev ID and content against the cached one for equality. For most
247  // content types, the output should not change during the lifetime of this cache.
248  // Clearing it can cause extra parses on edit for no reason.
249  }
250 
256  public function clearPreparedEdit() {
257  $this->mPreparedEdit = false;
258  }
259 
266  public static function selectFields() {
267  global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
268 
269  $fields = [
270  'page_id',
271  'page_namespace',
272  'page_title',
273  'page_restrictions',
274  'page_is_redirect',
275  'page_is_new',
276  'page_random',
277  'page_touched',
278  'page_links_updated',
279  'page_latest',
280  'page_len',
281  ];
282 
283  if ( $wgContentHandlerUseDB ) {
284  $fields[] = 'page_content_model';
285  }
286 
287  if ( $wgPageLanguageUseDB ) {
288  $fields[] = 'page_lang';
289  }
290 
291  return $fields;
292  }
293 
301  protected function pageData( $dbr, $conditions, $options = [] ) {
302  $fields = self::selectFields();
303 
304  // Avoid PHP 7.1 warning of passing $this by reference
305  $wikiPage = $this;
306 
307  Hooks::run( 'ArticlePageDataBefore', [ &$wikiPage, &$fields ] );
308 
309  $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
310 
311  Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
312 
313  return $row;
314  }
315 
325  public function pageDataFromTitle( $dbr, $title, $options = [] ) {
326  return $this->pageData( $dbr, [
327  'page_namespace' => $title->getNamespace(),
328  'page_title' => $title->getDBkey() ], $options );
329  }
330 
339  public function pageDataFromId( $dbr, $id, $options = [] ) {
340  return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
341  }
342 
355  public function loadPageData( $from = 'fromdb' ) {
356  $from = self::convertSelectType( $from );
357  if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
358  // We already have the data from the correct location, no need to load it twice.
359  return;
360  }
361 
362  if ( is_int( $from ) ) {
363  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
364  $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
365 
366  if ( !$data
367  && $index == DB_SLAVE
368  && wfGetLB()->getServerCount() > 1
369  && wfGetLB()->hasOrMadeRecentMasterChanges()
370  ) {
371  $from = self::READ_LATEST;
372  list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
373  $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
374  }
375  } else {
376  // No idea from where the caller got this data, assume slave database.
377  $data = $from;
378  $from = self::READ_NORMAL;
379  }
380 
381  $this->loadFromRow( $data, $from );
382  }
383 
395  public function loadFromRow( $data, $from ) {
396  $lc = LinkCache::singleton();
397  $lc->clearLink( $this->mTitle );
398 
399  if ( $data ) {
400  $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
401 
402  $this->mTitle->loadFromRow( $data );
403 
404  // Old-fashioned restrictions
405  $this->mTitle->loadRestrictions( $data->page_restrictions );
406 
407  $this->mId = intval( $data->page_id );
408  $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
409  $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
410  $this->mIsRedirect = intval( $data->page_is_redirect );
411  $this->mLatest = intval( $data->page_latest );
412  // Bug 37225: $latest may no longer match the cached latest Revision object.
413  // Double-check the ID of any cached latest Revision object for consistency.
414  if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
415  $this->mLastRevision = null;
416  $this->mTimestamp = '';
417  }
418  } else {
419  $lc->addBadLinkObj( $this->mTitle );
420 
421  $this->mTitle->loadFromRow( false );
422 
423  $this->clearCacheFields();
424 
425  $this->mId = 0;
426  }
427 
428  $this->mDataLoaded = true;
429  $this->mDataLoadedFrom = self::convertSelectType( $from );
430  }
431 
435  public function getId() {
436  if ( !$this->mDataLoaded ) {
437  $this->loadPageData();
438  }
439  return $this->mId;
440  }
441 
445  public function exists() {
446  if ( !$this->mDataLoaded ) {
447  $this->loadPageData();
448  }
449  return $this->mId > 0;
450  }
451 
460  public function hasViewableContent() {
461  return $this->exists() || $this->mTitle->isAlwaysKnown();
462  }
463 
469  public function isRedirect() {
470  if ( !$this->mDataLoaded ) {
471  $this->loadPageData();
472  }
473 
474  return (bool)$this->mIsRedirect;
475  }
476 
487  public function getContentModel() {
488  if ( $this->exists() ) {
489  // look at the revision's actual content model
490  $rev = $this->getRevision();
491 
492  if ( $rev !== null ) {
493  return $rev->getContentModel();
494  } else {
495  $title = $this->mTitle->getPrefixedDBkey();
496  wfWarn( "Page $title exists but has no (visible) revisions!" );
497  }
498  }
499 
500  // use the default model for this page
501  return $this->mTitle->getContentModel();
502  }
503 
508  public function checkTouched() {
509  if ( !$this->mDataLoaded ) {
510  $this->loadPageData();
511  }
512  return !$this->mIsRedirect;
513  }
514 
519  public function getTouched() {
520  if ( !$this->mDataLoaded ) {
521  $this->loadPageData();
522  }
523  return $this->mTouched;
524  }
525 
530  public function getLinksTimestamp() {
531  if ( !$this->mDataLoaded ) {
532  $this->loadPageData();
533  }
534  return $this->mLinksUpdated;
535  }
536 
541  public function getLatest() {
542  if ( !$this->mDataLoaded ) {
543  $this->loadPageData();
544  }
545  return (int)$this->mLatest;
546  }
547 
552  public function getOldestRevision() {
553 
554  // Try using the slave database first, then try the master
555  $continue = 2;
556  $db = wfGetDB( DB_SLAVE );
557  $revSelectFields = Revision::selectFields();
558 
559  $row = null;
560  while ( $continue ) {
561  $row = $db->selectRow(
562  [ 'page', 'revision' ],
563  $revSelectFields,
564  [
565  'page_namespace' => $this->mTitle->getNamespace(),
566  'page_title' => $this->mTitle->getDBkey(),
567  'rev_page = page_id'
568  ],
569  __METHOD__,
570  [
571  'ORDER BY' => 'rev_timestamp ASC'
572  ]
573  );
574 
575  if ( $row ) {
576  $continue = 0;
577  } else {
578  $db = wfGetDB( DB_MASTER );
579  $continue--;
580  }
581  }
582 
583  return $row ? Revision::newFromRow( $row ) : null;
584  }
585 
590  protected function loadLastEdit() {
591  if ( $this->mLastRevision !== null ) {
592  return; // already loaded
593  }
594 
595  $latest = $this->getLatest();
596  if ( !$latest ) {
597  return; // page doesn't exist or is missing page_latest info
598  }
599 
600  if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
601  // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always
602  // includes the latest changes committed. This is true even within REPEATABLE-READ
603  // transactions, where S1 normally only sees changes committed before the first S1
604  // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
605  // may not find it since a page row UPDATE and revision row INSERT by S2 may have
606  // happened after the first S1 SELECT.
607  // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
609  } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
610  // Bug T93976: if page_latest was loaded from the master, fetch the
611  // revision from there as well, as it may not exist yet on a slave DB.
612  // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
614  } else {
615  $flags = 0;
616  }
617  $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
618  if ( $revision ) { // sanity
619  $this->setLastEdit( $revision );
620  }
621  }
622 
627  protected function setLastEdit( Revision $revision ) {
628  $this->mLastRevision = $revision;
629  $this->mTimestamp = $revision->getTimestamp();
630  }
631 
636  public function getRevision() {
637  $this->loadLastEdit();
638  if ( $this->mLastRevision ) {
639  return $this->mLastRevision;
640  }
641  return null;
642  }
643 
657  public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
658  $this->loadLastEdit();
659  if ( $this->mLastRevision ) {
660  return $this->mLastRevision->getContent( $audience, $user );
661  }
662  return null;
663  }
664 
677  public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
678  ContentHandler::deprecated( __METHOD__, '1.21' );
679 
680  $this->loadLastEdit();
681  if ( $this->mLastRevision ) {
682  return $this->mLastRevision->getText( $audience, $user );
683  }
684  return false;
685  }
686 
690  public function getTimestamp() {
691  // Check if the field has been filled by WikiPage::setTimestamp()
692  if ( !$this->mTimestamp ) {
693  $this->loadLastEdit();
694  }
695 
696  return wfTimestamp( TS_MW, $this->mTimestamp );
697  }
698 
704  public function setTimestamp( $ts ) {
705  $this->mTimestamp = wfTimestamp( TS_MW, $ts );
706  }
707 
717  public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
718  $this->loadLastEdit();
719  if ( $this->mLastRevision ) {
720  return $this->mLastRevision->getUser( $audience, $user );
721  } else {
722  return -1;
723  }
724  }
725 
736  public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
737  $revision = $this->getOldestRevision();
738  if ( $revision ) {
739  $userName = $revision->getUserText( $audience, $user );
740  return User::newFromName( $userName, false );
741  } else {
742  return null;
743  }
744  }
745 
755  public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
756  $this->loadLastEdit();
757  if ( $this->mLastRevision ) {
758  return $this->mLastRevision->getUserText( $audience, $user );
759  } else {
760  return '';
761  }
762  }
763 
773  public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
774  $this->loadLastEdit();
775  if ( $this->mLastRevision ) {
776  return $this->mLastRevision->getComment( $audience, $user );
777  } else {
778  return '';
779  }
780  }
781 
787  public function getMinorEdit() {
788  $this->loadLastEdit();
789  if ( $this->mLastRevision ) {
790  return $this->mLastRevision->isMinor();
791  } else {
792  return false;
793  }
794  }
795 
804  public function isCountable( $editInfo = false ) {
805  global $wgArticleCountMethod;
806 
807  if ( !$this->mTitle->isContentPage() ) {
808  return false;
809  }
810 
811  if ( $editInfo ) {
812  $content = $editInfo->pstContent;
813  } else {
814  $content = $this->getContent();
815  }
816 
817  if ( !$content || $content->isRedirect() ) {
818  return false;
819  }
820 
821  $hasLinks = null;
822 
823  if ( $wgArticleCountMethod === 'link' ) {
824  // nasty special case to avoid re-parsing to detect links
825 
826  if ( $editInfo ) {
827  // ParserOutput::getLinks() is a 2D array of page links, so
828  // to be really correct we would need to recurse in the array
829  // but the main array should only have items in it if there are
830  // links.
831  $hasLinks = (bool)count( $editInfo->output->getLinks() );
832  } else {
833  $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
834  [ 'pl_from' => $this->getId() ], __METHOD__ );
835  }
836  }
837 
838  return $content->isCountable( $hasLinks );
839  }
840 
848  public function getRedirectTarget() {
849  if ( !$this->mTitle->isRedirect() ) {
850  return null;
851  }
852 
853  if ( $this->mRedirectTarget !== null ) {
854  return $this->mRedirectTarget;
855  }
856 
857  // Query the redirect table
858  $dbr = wfGetDB( DB_SLAVE );
859  $row = $dbr->selectRow( 'redirect',
860  [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
861  [ 'rd_from' => $this->getId() ],
862  __METHOD__
863  );
864 
865  // rd_fragment and rd_interwiki were added later, populate them if empty
866  if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
867  $this->mRedirectTarget = Title::makeTitle(
868  $row->rd_namespace, $row->rd_title,
869  $row->rd_fragment, $row->rd_interwiki
870  );
871  return $this->mRedirectTarget;
872  }
873 
874  // This page doesn't have an entry in the redirect table
875  $this->mRedirectTarget = $this->insertRedirect();
876  return $this->mRedirectTarget;
877  }
878 
887  public function insertRedirect() {
888  $content = $this->getContent();
889  $retval = $content ? $content->getUltimateRedirectTarget() : null;
890  if ( !$retval ) {
891  return null;
892  }
893 
894  // Update the DB post-send if the page has not cached since now
895  $that = $this;
896  $latest = $this->getLatest();
897  DeferredUpdates::addCallableUpdate( function() use ( $that, $retval, $latest ) {
898  $that->insertRedirectEntry( $retval, $latest );
899  } );
900 
901  return $retval;
902  }
903 
909  public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
910  $dbw = wfGetDB( DB_MASTER );
911  $dbw->startAtomic( __METHOD__ );
912 
913  if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
914  $dbw->replace( 'redirect',
915  [ 'rd_from' ],
916  [
917  'rd_from' => $this->getId(),
918  'rd_namespace' => $rt->getNamespace(),
919  'rd_title' => $rt->getDBkey(),
920  'rd_fragment' => $rt->getFragment(),
921  'rd_interwiki' => $rt->getInterwiki(),
922  ],
923  __METHOD__
924  );
925  }
926 
927  $dbw->endAtomic( __METHOD__ );
928  }
929 
935  public function followRedirect() {
936  return $this->getRedirectURL( $this->getRedirectTarget() );
937  }
938 
946  public function getRedirectURL( $rt ) {
947  if ( !$rt ) {
948  return false;
949  }
950 
951  if ( $rt->isExternal() ) {
952  if ( $rt->isLocal() ) {
953  // Offsite wikis need an HTTP redirect.
954  // This can be hard to reverse and may produce loops,
955  // so they may be disabled in the site configuration.
956  $source = $this->mTitle->getFullURL( 'redirect=no' );
957  return $rt->getFullURL( [ 'rdfrom' => $source ] );
958  } else {
959  // External pages without "local" bit set are not valid
960  // redirect targets
961  return false;
962  }
963  }
964 
965  if ( $rt->isSpecialPage() ) {
966  // Gotta handle redirects to special pages differently:
967  // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
968  // Some pages are not valid targets.
969  if ( $rt->isValidRedirectTarget() ) {
970  return $rt->getFullURL();
971  } else {
972  return false;
973  }
974  }
975 
976  return $rt;
977  }
978 
984  public function getContributors() {
985  // @todo FIXME: This is expensive; cache this info somewhere.
986 
987  $dbr = wfGetDB( DB_SLAVE );
988 
989  if ( $dbr->implicitGroupby() ) {
990  $realNameField = 'user_real_name';
991  } else {
992  $realNameField = 'MIN(user_real_name) AS user_real_name';
993  }
994 
995  $tables = [ 'revision', 'user' ];
996 
997  $fields = [
998  'user_id' => 'rev_user',
999  'user_name' => 'rev_user_text',
1000  $realNameField,
1001  'timestamp' => 'MAX(rev_timestamp)',
1002  ];
1003 
1004  $conds = [ 'rev_page' => $this->getId() ];
1005 
1006  // The user who made the top revision gets credited as "this page was last edited by
1007  // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1008  $user = $this->getUser();
1009  if ( $user ) {
1010  $conds[] = "rev_user != $user";
1011  } else {
1012  $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
1013  }
1014 
1015  // Username hidden?
1016  $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1017 
1018  $jconds = [
1019  'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
1020  ];
1021 
1022  $options = [
1023  'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
1024  'ORDER BY' => 'timestamp DESC',
1025  ];
1026 
1027  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1028  return new UserArrayFromResult( $res );
1029  }
1030 
1038  public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1039  return $parserOptions->getStubThreshold() == 0
1040  && $this->exists()
1041  && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1042  && $this->getContentHandler()->isParserCacheSupported();
1043  }
1044 
1058  public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
1059 
1060  $useParserCache = $this->shouldCheckParserCache( $parserOptions, $oldid );
1061  wfDebug( __METHOD__ .
1062  ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1063  if ( $parserOptions->getStubThreshold() ) {
1064  wfIncrStats( 'pcache.miss.stub' );
1065  }
1066 
1067  if ( $useParserCache ) {
1068  $parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
1069  if ( $parserOutput !== false ) {
1070  return $parserOutput;
1071  }
1072  }
1073 
1074  if ( $oldid === null || $oldid === 0 ) {
1075  $oldid = $this->getLatest();
1076  }
1077 
1078  $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1079  $pool->execute();
1080 
1081  return $pool->getParserOutput();
1082  }
1083 
1089  public function doViewUpdates( User $user, $oldid = 0 ) {
1090  if ( wfReadOnly() ) {
1091  return;
1092  }
1093 
1094  Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1095  // Update newtalk / watchlist notification status
1096  try {
1097  $user->clearNotification( $this->mTitle, $oldid );
1098  } catch ( DBError $e ) {
1099  // Avoid outage if the master is not reachable
1101  }
1102  }
1103 
1108  public function doPurge() {
1109  // Avoid PHP 7.1 warning of passing $this by reference
1110  $wikiPage = $this;
1111 
1112  if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1113  return false;
1114  }
1115 
1117  wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) {
1118  // Invalidate the cache in auto-commit mode
1119  $title->invalidateCache();
1120  } );
1121 
1122  // Send purge after above page_touched update was committed
1124  new CdnCacheUpdate( $title->getCdnUrls() ),
1126  );
1127 
1128  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1129  // @todo move this logic to MessageCache
1130  if ( $this->exists() ) {
1131  // NOTE: use transclusion text for messages.
1132  // This is consistent with MessageCache::getMsgFromNamespace()
1133 
1134  $content = $this->getContent();
1135  $text = $content === null ? null : $content->getWikitextForTransclusion();
1136 
1137  if ( $text === null ) {
1138  $text = false;
1139  }
1140  } else {
1141  $text = false;
1142  }
1143 
1144  MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
1145  }
1146 
1147  return true;
1148  }
1149 
1162  public function insertOn( $dbw, $pageId = null ) {
1163  $pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
1164  $dbw->insert(
1165  'page',
1166  [
1167  'page_id' => $pageIdForInsert,
1168  'page_namespace' => $this->mTitle->getNamespace(),
1169  'page_title' => $this->mTitle->getDBkey(),
1170  'page_restrictions' => '',
1171  'page_is_redirect' => 0, // Will set this shortly...
1172  'page_is_new' => 1,
1173  'page_random' => wfRandom(),
1174  'page_touched' => $dbw->timestamp(),
1175  'page_latest' => 0, // Fill this in shortly...
1176  'page_len' => 0, // Fill this in shortly...
1177  ],
1178  __METHOD__,
1179  'IGNORE'
1180  );
1181 
1182  if ( $dbw->affectedRows() > 0 ) {
1183  $newid = $pageId ?: $dbw->insertId();
1184  $this->mId = $newid;
1185  $this->mTitle->resetArticleID( $newid );
1186 
1187  return $newid;
1188  } else {
1189  return false; // nothing changed
1190  }
1191  }
1192 
1206  public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1207  $lastRevIsRedirect = null
1208  ) {
1209  global $wgContentHandlerUseDB;
1210 
1211  // Assertion to try to catch T92046
1212  if ( (int)$revision->getId() === 0 ) {
1213  throw new InvalidArgumentException(
1214  __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1215  );
1216  }
1217 
1218  $content = $revision->getContent();
1219  $len = $content ? $content->getSize() : 0;
1220  $rt = $content ? $content->getUltimateRedirectTarget() : null;
1221 
1222  $conditions = [ 'page_id' => $this->getId() ];
1223 
1224  if ( !is_null( $lastRevision ) ) {
1225  // An extra check against threads stepping on each other
1226  $conditions['page_latest'] = $lastRevision;
1227  }
1228 
1229  $row = [ /* SET */
1230  'page_latest' => $revision->getId(),
1231  'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1232  'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1233  'page_is_redirect' => $rt !== null ? 1 : 0,
1234  'page_len' => $len,
1235  ];
1236 
1237  if ( $wgContentHandlerUseDB ) {
1238  $row['page_content_model'] = $revision->getContentModel();
1239  }
1240 
1241  $dbw->update( 'page',
1242  $row,
1243  $conditions,
1244  __METHOD__ );
1245 
1246  $result = $dbw->affectedRows() > 0;
1247  if ( $result ) {
1248  $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1249  $this->setLastEdit( $revision );
1250  $this->mLatest = $revision->getId();
1251  $this->mIsRedirect = (bool)$rt;
1252  // Update the LinkCache.
1253  LinkCache::singleton()->addGoodLinkObj(
1254  $this->getId(),
1255  $this->mTitle,
1256  $len,
1257  $this->mIsRedirect,
1258  $this->mLatest,
1259  $revision->getContentModel()
1260  );
1261  }
1262 
1263  return $result;
1264  }
1265 
1277  public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1278  // Always update redirects (target link might have changed)
1279  // Update/Insert if we don't know if the last revision was a redirect or not
1280  // Delete if changing from redirect to non-redirect
1281  $isRedirect = !is_null( $redirectTitle );
1282 
1283  if ( !$isRedirect && $lastRevIsRedirect === false ) {
1284  return true;
1285  }
1286 
1287  if ( $isRedirect ) {
1288  $this->insertRedirectEntry( $redirectTitle );
1289  } else {
1290  // This is not a redirect, remove row from redirect table
1291  $where = [ 'rd_from' => $this->getId() ];
1292  $dbw->delete( 'redirect', $where, __METHOD__ );
1293  }
1294 
1295  if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1296  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1297  }
1298 
1299  return ( $dbw->affectedRows() != 0 );
1300  }
1301 
1312  public function updateIfNewerOn( $dbw, $revision ) {
1313 
1314  $row = $dbw->selectRow(
1315  [ 'revision', 'page' ],
1316  [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1317  [
1318  'page_id' => $this->getId(),
1319  'page_latest=rev_id' ],
1320  __METHOD__ );
1321 
1322  if ( $row ) {
1323  if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1324  return false;
1325  }
1326  $prev = $row->rev_id;
1327  $lastRevIsRedirect = (bool)$row->page_is_redirect;
1328  } else {
1329  // No or missing previous revision; mark the page as new
1330  $prev = 0;
1331  $lastRevIsRedirect = null;
1332  }
1333 
1334  $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1335 
1336  return $ret;
1337  }
1338 
1349  public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
1350  $handler = $undo->getContentHandler();
1351  return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1352  }
1353 
1364  public function supportsSections() {
1365  return $this->getContentHandler()->supportsSections();
1366  }
1367 
1382  public function replaceSectionContent(
1383  $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1384  ) {
1385 
1386  $baseRevId = null;
1387  if ( $edittime && $sectionId !== 'new' ) {
1388  $dbr = wfGetDB( DB_SLAVE );
1389  $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1390  // Try the master if this thread may have just added it.
1391  // This could be abstracted into a Revision method, but we don't want
1392  // to encourage loading of revisions by timestamp.
1393  if ( !$rev
1394  && wfGetLB()->getServerCount() > 1
1395  && wfGetLB()->hasOrMadeRecentMasterChanges()
1396  ) {
1397  $dbw = wfGetDB( DB_MASTER );
1398  $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1399  }
1400  if ( $rev ) {
1401  $baseRevId = $rev->getId();
1402  }
1403  }
1404 
1405  return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1406  }
1407 
1421  public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1422  $sectionTitle = '', $baseRevId = null
1423  ) {
1424 
1425  if ( strval( $sectionId ) === '' ) {
1426  // Whole-page edit; let the whole text through
1427  $newContent = $sectionContent;
1428  } else {
1429  if ( !$this->supportsSections() ) {
1430  throw new MWException( "sections not supported for content model " .
1431  $this->getContentHandler()->getModelID() );
1432  }
1433 
1434  // Bug 30711: always use current version when adding a new section
1435  if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1436  $oldContent = $this->getContent();
1437  } else {
1438  $rev = Revision::newFromId( $baseRevId );
1439  if ( !$rev ) {
1440  wfDebug( __METHOD__ . " asked for bogus section (page: " .
1441  $this->getId() . "; section: $sectionId)\n" );
1442  return null;
1443  }
1444 
1445  $oldContent = $rev->getContent();
1446  }
1447 
1448  if ( !$oldContent ) {
1449  wfDebug( __METHOD__ . ": no page text\n" );
1450  return null;
1451  }
1452 
1453  $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1454  }
1455 
1456  return $newContent;
1457  }
1458 
1464  public function checkFlags( $flags ) {
1465  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1466  if ( $this->exists() ) {
1467  $flags |= EDIT_UPDATE;
1468  } else {
1469  $flags |= EDIT_NEW;
1470  }
1471  }
1472 
1473  return $flags;
1474  }
1475 
1528  public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
1529  ContentHandler::deprecated( __METHOD__, '1.21' );
1530 
1531  $content = ContentHandler::makeContent( $text, $this->getTitle() );
1532 
1533  return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
1534  }
1535 
1591  public function doEditContent(
1592  Content $content, $summary, $flags = 0, $baseRevId = false,
1593  User $user = null, $serialFormat = null, $tags = null
1594  ) {
1595  global $wgUser, $wgUseAutomaticEditSummaries;
1596 
1597  // Low-level sanity check
1598  if ( $this->mTitle->getText() === '' ) {
1599  throw new MWException( 'Something is trying to edit an article with an empty title' );
1600  }
1601  // Make sure the given content type is allowed for this page
1602  if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
1603  return Status::newFatal( 'content-not-allowed-here',
1605  $this->mTitle->getPrefixedText()
1606  );
1607  }
1608 
1609  // Load the data from the master database if needed.
1610  // The caller may already loaded it from the master or even loaded it using
1611  // SELECT FOR UPDATE, so do not override that using clear().
1612  $this->loadPageData( 'fromdbmaster' );
1613 
1614  $user = $user ?: $wgUser;
1615  $flags = $this->checkFlags( $flags );
1616 
1617  // Avoid PHP 7.1 warning of passing $this by reference
1618  $wikiPage = $this;
1619 
1620  // Trigger pre-save hook (using provided edit summary)
1621  $hookStatus = Status::newGood( [] );
1622  $hook_args = [ &$wikiPage, &$user, &$content, &$summary,
1623  $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
1624  // Check if the hook rejected the attempted save
1625  if ( !Hooks::run( 'PageContentSave', $hook_args )
1626  || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args )
1627  ) {
1628  if ( $hookStatus->isOK() ) {
1629  // Hook returned false but didn't call fatal(); use generic message
1630  $hookStatus->fatal( 'edit-hook-aborted' );
1631  }
1632 
1633  return $hookStatus;
1634  }
1635 
1636  $old_revision = $this->getRevision(); // current revision
1637  $old_content = $this->getContent( Revision::RAW ); // current revision's content
1638 
1639  // Provide autosummaries if one is not provided and autosummaries are enabled
1640  if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
1641  $handler = $content->getContentHandler();
1642  $summary = $handler->getAutosummary( $old_content, $content, $flags );
1643  }
1644 
1645  // Get the pre-save transform content and final parser output
1646  $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat );
1647  $pstContent = $editInfo->pstContent; // Content object
1648  $meta = [
1649  'bot' => ( $flags & EDIT_FORCE_BOT ),
1650  'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
1651  'serialized' => $editInfo->pst,
1652  'serialFormat' => $serialFormat,
1653  'baseRevId' => $baseRevId,
1654  'oldRevision' => $old_revision,
1655  'oldContent' => $old_content,
1656  'oldId' => $this->getLatest(),
1657  'oldIsRedirect' => $this->isRedirect(),
1658  'oldCountable' => $this->isCountable(),
1659  'tags' => ( $tags !== null ) ? (array)$tags : []
1660  ];
1661 
1662  // Actually create the revision and create/update the page
1663  if ( $flags & EDIT_UPDATE ) {
1664  $status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
1665  } else {
1666  $status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
1667  }
1668 
1669  // Promote user to any groups they meet the criteria for
1670  DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1671  $user->addAutopromoteOnceGroups( 'onEdit' );
1672  $user->addAutopromoteOnceGroups( 'onView' ); // b/c
1673  } );
1674 
1675  return $status;
1676  }
1677 
1690  private function doModify(
1692  ) {
1693  global $wgUseRCPatrol;
1694 
1695  // Update article, but only if changed.
1696  $status = Status::newGood( [ 'new' => false, 'revision' => null ] );
1697 
1698  // Convenience variables
1699  $now = wfTimestampNow();
1700  $oldid = $meta['oldId'];
1702  $oldContent = $meta['oldContent'];
1703  $newsize = $content->getSize();
1704 
1705  if ( !$oldid ) {
1706  // Article gone missing
1707  $status->fatal( 'edit-gone-missing' );
1708 
1709  return $status;
1710  } elseif ( !$oldContent ) {
1711  // Sanity check for bug 37225
1712  throw new MWException( "Could not find text for current revision {$oldid}." );
1713  }
1714 
1715  // @TODO: pass content object?!
1716  $revision = new Revision( [
1717  'page' => $this->getId(),
1718  'title' => $this->mTitle, // for determining the default content model
1719  'comment' => $summary,
1720  'minor_edit' => $meta['minor'],
1721  'text' => $meta['serialized'],
1722  'len' => $newsize,
1723  'parent_id' => $oldid,
1724  'user' => $user->getId(),
1725  'user_text' => $user->getName(),
1726  'timestamp' => $now,
1727  'content_model' => $content->getModel(),
1728  'content_format' => $meta['serialFormat'],
1729  ] );
1730 
1731  $changed = !$content->equals( $oldContent );
1732 
1733  $dbw = wfGetDB( DB_MASTER );
1734 
1735  if ( $changed ) {
1736  $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
1737  $status->merge( $prepStatus );
1738  if ( !$status->isOK() ) {
1739  return $status;
1740  }
1741 
1742  $dbw->startAtomic( __METHOD__ );
1743  // Get the latest page_latest value while locking it.
1744  // Do a CAS style check to see if it's the same as when this method
1745  // started. If it changed then bail out before touching the DB.
1746  $latestNow = $this->lockAndGetLatest();
1747  if ( $latestNow != $oldid ) {
1748  $dbw->endAtomic( __METHOD__ );
1749  // Page updated or deleted in the mean time
1750  $status->fatal( 'edit-conflict' );
1751 
1752  return $status;
1753  }
1754 
1755  // At this point we are now comitted to returning an OK
1756  // status unless some DB query error or other exception comes up.
1757  // This way callers don't have to call rollback() if $status is bad
1758  // unless they actually try to catch exceptions (which is rare).
1759 
1760  // Save the revision text
1761  $revisionId = $revision->insertOn( $dbw );
1762  // Update page_latest and friends to reflect the new revision
1763  if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
1764  $dbw->rollback( __METHOD__ ); // sanity; this should never happen
1765  throw new MWException( "Failed to update page row to use new revision." );
1766  }
1767 
1768  Hooks::run( 'NewRevisionFromEditComplete',
1769  [ $this, $revision, $meta['baseRevId'], $user ] );
1770 
1771  // Update recentchanges
1772  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1773  // Mark as patrolled if the user can do so
1774  $patrolled = $wgUseRCPatrol && !count(
1775  $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1776  // Add RC row to the DB
1778  $now,
1779  $this->mTitle,
1780  $revision->isMinor(),
1781  $user,
1782  $summary,
1783  $oldid,
1784  $this->getTimestamp(),
1785  $meta['bot'],
1786  '',
1787  $oldContent ? $oldContent->getSize() : 0,
1788  $newsize,
1789  $revisionId,
1790  $patrolled,
1791  $meta['tags']
1792  );
1793  }
1794 
1795  $user->incEditCount();
1796 
1797  $dbw->endAtomic( __METHOD__ );
1798  $this->mTimestamp = $now;
1799  } else {
1800  // Bug 32948: revision ID must be set to page {{REVISIONID}} and
1801  // related variables correctly
1802  $revision->setId( $this->getLatest() );
1803  }
1804 
1805  if ( $changed ) {
1806  // Return the new revision to the caller
1807  $status->value['revision'] = $revision;
1808  } else {
1809  $status->warning( 'edit-no-change' );
1810  // Update page_touched as updateRevisionOn() was not called.
1811  // Other cache updates are managed in onArticleEdit() via doEditUpdates().
1812  $this->mTitle->invalidateCache( $now );
1813  }
1814 
1815  // Do secondary updates once the main changes have been committed...
1816  $that = $this;
1817  $dbw->onTransactionIdle(
1818  function () use (
1819  $dbw, &$that, $revision, &$user, $content, $summary, &$flags,
1820  $changed, $meta, &$status
1821  ) {
1822  // Do per-page updates in a transaction
1823  $dbw->setFlag( DBO_TRX );
1824  // Update links tables, site stats, etc.
1825  $that->doEditUpdates(
1826  $revision,
1827  $user,
1828  [
1829  'changed' => $changed,
1830  'oldcountable' => $meta['oldCountable'],
1831  'oldrevision' => $meta['oldRevision']
1832  ]
1833  );
1834  // Trigger post-save hook
1835  $params = [ &$that, &$user, $content, $summary, $flags & EDIT_MINOR,
1836  null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
1837  ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1838  Hooks::run( 'PageContentSaveComplete', $params );
1839  }
1840  );
1841 
1842  return $status;
1843  }
1844 
1857  private function doCreate(
1859  ) {
1860  global $wgUseRCPatrol, $wgUseNPPatrol;
1861 
1862  $status = Status::newGood( [ 'new' => true, 'revision' => null ] );
1863 
1864  $now = wfTimestampNow();
1865  $newsize = $content->getSize();
1866  $prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
1867  $status->merge( $prepStatus );
1868  if ( !$status->isOK() ) {
1869  return $status;
1870  }
1871 
1872  $dbw = wfGetDB( DB_MASTER );
1873  $dbw->startAtomic( __METHOD__ );
1874 
1875  // Add the page record unless one already exists for the title
1876  $newid = $this->insertOn( $dbw );
1877  if ( $newid === false ) {
1878  $dbw->endAtomic( __METHOD__ ); // nothing inserted
1879  $status->fatal( 'edit-already-exists' );
1880 
1881  return $status; // nothing done
1882  }
1883 
1884  // At this point we are now comitted to returning an OK
1885  // status unless some DB query error or other exception comes up.
1886  // This way callers don't have to call rollback() if $status is bad
1887  // unless they actually try to catch exceptions (which is rare).
1888 
1889  // @TODO: pass content object?!
1890  $revision = new Revision( [
1891  'page' => $newid,
1892  'title' => $this->mTitle, // for determining the default content model
1893  'comment' => $summary,
1894  'minor_edit' => $meta['minor'],
1895  'text' => $meta['serialized'],
1896  'len' => $newsize,
1897  'user' => $user->getId(),
1898  'user_text' => $user->getName(),
1899  'timestamp' => $now,
1900  'content_model' => $content->getModel(),
1901  'content_format' => $meta['serialFormat'],
1902  ] );
1903 
1904  // Save the revision text...
1905  $revisionId = $revision->insertOn( $dbw );
1906  // Update the page record with revision data
1907  if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
1908  $dbw->rollback( __METHOD__ ); // sanity; this should never happen
1909  throw new MWException( "Failed to update page row to use new revision." );
1910  }
1911 
1912  Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
1913 
1914  // Update recentchanges
1915  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1916  // Mark as patrolled if the user can do so
1917  $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
1918  !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1919  // Add RC row to the DB
1921  $now,
1922  $this->mTitle,
1923  $revision->isMinor(),
1924  $user,
1925  $summary,
1926  $meta['bot'],
1927  '',
1928  $newsize,
1929  $revisionId,
1930  $patrolled,
1931  $meta['tags']
1932  );
1933  }
1934 
1935  $user->incEditCount();
1936 
1937  $dbw->endAtomic( __METHOD__ );
1938  $this->mTimestamp = $now;
1939 
1940  // Return the new revision to the caller
1941  $status->value['revision'] = $revision;
1942 
1943  // Do secondary updates once the main changes have been committed...
1944  $that = $this;
1945  $dbw->onTransactionIdle(
1946  function () use (
1947  &$that, $dbw, $revision, &$user, $content, $summary, &$flags, $meta, &$status
1948  ) {
1949  // Do per-page updates in a transaction
1950  $dbw->setFlag( DBO_TRX );
1951  // Update links, etc.
1952  $that->doEditUpdates( $revision, $user, [ 'created' => true ] );
1953  // Trigger post-create hook
1954  $params = [ &$that, &$user, $content, $summary,
1955  $flags & EDIT_MINOR, null, null, &$flags, $revision ];
1956  ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params );
1957  Hooks::run( 'PageContentInsertComplete', $params );
1958  // Trigger post-save hook
1959  $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
1960  ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1961  Hooks::run( 'PageContentSaveComplete', $params );
1962  }
1963  );
1964 
1965  return $status;
1966  }
1967 
1982  public function makeParserOptions( $context ) {
1983  $options = $this->getContentHandler()->makeParserOptions( $context );
1984 
1985  if ( $this->getTitle()->isConversionTable() ) {
1986  // @todo ConversionTable should become a separate content model, so
1987  // we don't need special cases like this one.
1988  $options->disableContentConversion();
1989  }
1990 
1991  return $options;
1992  }
1993 
2004  public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
2005  ContentHandler::deprecated( __METHOD__, '1.21' );
2006  $content = ContentHandler::makeContent( $text, $this->getTitle() );
2007  return $this->prepareContentForEdit( $content, $revid, $user );
2008  }
2009 
2025  public function prepareContentForEdit(
2026  Content $content, $revision = null, User $user = null,
2027  $serialFormat = null, $useCache = true
2028  ) {
2029  global $wgContLang, $wgUser, $wgAjaxEditStash;
2030 
2031  if ( is_object( $revision ) ) {
2032  $revid = $revision->getId();
2033  } else {
2034  $revid = $revision;
2035  // This code path is deprecated, and nothing is known to
2036  // use it, so performance here shouldn't be a worry.
2037  if ( $revid !== null ) {
2038  $revision = Revision::newFromId( $revid, Revision::READ_LATEST );
2039  } else {
2040  $revision = null;
2041  }
2042  }
2043 
2044  $user = is_null( $user ) ? $wgUser : $user;
2045  // XXX: check $user->getId() here???
2046 
2047  // Use a sane default for $serialFormat, see bug 57026
2048  if ( $serialFormat === null ) {
2049  $serialFormat = $content->getContentHandler()->getDefaultFormat();
2050  }
2051 
2052  if ( $this->mPreparedEdit
2053  && $this->mPreparedEdit->newContent
2054  && $this->mPreparedEdit->newContent->equals( $content )
2055  && $this->mPreparedEdit->revid == $revid
2056  && $this->mPreparedEdit->format == $serialFormat
2057  // XXX: also check $user here?
2058  ) {
2059  // Already prepared
2060  return $this->mPreparedEdit;
2061  }
2062 
2063  // The edit may have already been prepared via api.php?action=stashedit
2064  $cachedEdit = $useCache && $wgAjaxEditStash
2065  ? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
2066  : false;
2067 
2068  $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
2069  Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
2070 
2071  $edit = (object)[];
2072  if ( $cachedEdit ) {
2073  $edit->timestamp = $cachedEdit->timestamp;
2074  } else {
2075  $edit->timestamp = wfTimestampNow();
2076  }
2077  // @note: $cachedEdit is not used if the rev ID was referenced in the text
2078  $edit->revid = $revid;
2079 
2080  if ( $cachedEdit ) {
2081  $edit->pstContent = $cachedEdit->pstContent;
2082  } else {
2083  $edit->pstContent = $content
2084  ? $content->preSaveTransform( $this->mTitle, $user, $popts )
2085  : null;
2086  }
2087 
2088  $edit->format = $serialFormat;
2089  $edit->popts = $this->makeParserOptions( 'canonical' );
2090  if ( $cachedEdit ) {
2091  $edit->output = $cachedEdit->output;
2092  } else {
2093  if ( $revision ) {
2094  // We get here if vary-revision is set. This means that this page references
2095  // itself (such as via self-transclusion). In this case, we need to make sure
2096  // that any such self-references refer to the newly-saved revision, and not
2097  // to the previous one, which could otherwise happen due to slave lag.
2098  $oldCallback = $edit->popts->getCurrentRevisionCallback();
2099  $edit->popts->setCurrentRevisionCallback(
2100  function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
2101  if ( $title->equals( $revision->getTitle() ) ) {
2102  return $revision;
2103  } else {
2104  return call_user_func( $oldCallback, $title, $parser );
2105  }
2106  }
2107  );
2108  }
2109  $edit->output = $edit->pstContent
2110  ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2111  : null;
2112  }
2113 
2114  $edit->newContent = $content;
2115  $edit->oldContent = $this->getContent( Revision::RAW );
2116 
2117  // NOTE: B/C for hooks! don't use these fields!
2118  $edit->newText = $edit->newContent
2119  ? ContentHandler::getContentText( $edit->newContent )
2120  : '';
2121  $edit->oldText = $edit->oldContent
2122  ? ContentHandler::getContentText( $edit->oldContent )
2123  : '';
2124  $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
2125 
2126  if ( $edit->output ) {
2127  $edit->output->setCacheTime( wfTimestampNow() );
2128  }
2129 
2130  // Process cache the result
2131  $this->mPreparedEdit = $edit;
2132 
2133  return $edit;
2134  }
2135 
2157  public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2158  global $wgRCWatchCategoryMembership, $wgContLang;
2159 
2160  $options += [
2161  'changed' => true,
2162  'created' => false,
2163  'moved' => false,
2164  'restored' => false,
2165  'oldrevision' => null,
2166  'oldcountable' => null
2167  ];
2168  $content = $revision->getContent();
2169 
2170  // Parse the text
2171  // Be careful not to do pre-save transform twice: $text is usually
2172  // already pre-save transformed once.
2173  if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
2174  wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
2175  $editInfo = $this->prepareContentForEdit( $content, $revision, $user );
2176  } else {
2177  wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
2178  $editInfo = $this->mPreparedEdit;
2179  }
2180 
2181  // Save it to the parser cache.
2182  // Make sure the cache time matches page_touched to avoid double parsing.
2183  ParserCache::singleton()->save(
2184  $editInfo->output, $this, $editInfo->popts,
2185  $revision->getTimestamp(), $editInfo->revid
2186  );
2187 
2188  // Update the links tables and other secondary data
2189  if ( $content ) {
2190  $recursive = $options['changed']; // bug 50785
2191  $updates = $content->getSecondaryDataUpdates(
2192  $this->getTitle(), null, $recursive, $editInfo->output
2193  );
2194  foreach ( $updates as $update ) {
2195  if ( $update instanceof LinksUpdate ) {
2196  $update->setRevision( $revision );
2197  $update->setTriggeringUser( $user );
2198  }
2199  DeferredUpdates::addUpdate( $update );
2200  }
2201  if ( $wgRCWatchCategoryMembership
2202  && $this->getContentHandler()->supportsCategories() === true
2203  && ( $options['changed'] || $options['created'] )
2204  && !$options['restored']
2205  ) {
2206  // Note: jobs are pushed after deferred updates, so the job should be able to see
2207  // the recent change entry (also done via deferred updates) and carry over any
2208  // bot/deletion/IP flags, ect.
2210  $this->getTitle(),
2211  [
2212  'pageId' => $this->getId(),
2213  'revTimestamp' => $revision->getTimestamp()
2214  ]
2215  ) );
2216  }
2217  }
2218 
2219  // Avoid PHP 7.1 warning of passing $this by reference
2220  $wikiPage = $this;
2221 
2222  Hooks::run( 'ArticleEditUpdates', [ &$wikiPage, &$editInfo, $options['changed'] ] );
2223 
2224  if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
2225  // Flush old entries from the `recentchanges` table
2226  if ( mt_rand( 0, 9 ) == 0 ) {
2228  }
2229  }
2230 
2231  if ( !$this->exists() ) {
2232  return;
2233  }
2234 
2235  $id = $this->getId();
2236  $title = $this->mTitle->getPrefixedDBkey();
2237  $shortTitle = $this->mTitle->getDBkey();
2238 
2239  if ( $options['oldcountable'] === 'no-change' ||
2240  ( !$options['changed'] && !$options['moved'] )
2241  ) {
2242  $good = 0;
2243  } elseif ( $options['created'] ) {
2244  $good = (int)$this->isCountable( $editInfo );
2245  } elseif ( $options['oldcountable'] !== null ) {
2246  $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
2247  } else {
2248  $good = 0;
2249  }
2250  $edits = $options['changed'] ? 1 : 0;
2251  $total = $options['created'] ? 1 : 0;
2252 
2253  DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
2255 
2256  // If this is another user's talk page, update newtalk.
2257  // Don't do this if $options['changed'] = false (null-edits) nor if
2258  // it's a minor edit and the user doesn't want notifications for those.
2259  if ( $options['changed']
2260  && $this->mTitle->getNamespace() == NS_USER_TALK
2261  && $shortTitle != $user->getTitleKey()
2262  && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
2263  ) {
2264  $recipient = User::newFromName( $shortTitle, false );
2265  if ( !$recipient ) {
2266  wfDebug( __METHOD__ . ": invalid username\n" );
2267  } else {
2268  // Avoid PHP 7.1 warning of passing $this by reference
2269  $wikiPage = $this;
2270 
2271  // Allow extensions to prevent user notification
2272  // when a new message is added to their talk page
2273  if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
2274  if ( User::isIP( $shortTitle ) ) {
2275  // An anonymous user
2276  $recipient->setNewtalk( true, $revision );
2277  } elseif ( $recipient->isLoggedIn() ) {
2278  $recipient->setNewtalk( true, $revision );
2279  } else {
2280  wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
2281  }
2282  }
2283  }
2284  }
2285 
2286  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
2287  // XXX: could skip pseudo-messages like js/css here, based on content model.
2288  $msgtext = $content ? $content->getWikitextForTransclusion() : null;
2289  if ( $msgtext === false || $msgtext === null ) {
2290  $msgtext = '';
2291  }
2292 
2293  MessageCache::singleton()->replace( $shortTitle, $msgtext );
2294 
2295  if ( $wgContLang->hasVariants() ) {
2296  $wgContLang->updateConversionTable( $this->mTitle );
2297  }
2298  }
2299 
2300  if ( $options['created'] ) {
2301  self::onArticleCreate( $this->mTitle );
2302  } elseif ( $options['changed'] ) { // bug 50785
2303  self::onArticleEdit( $this->mTitle, $revision );
2304  }
2305  }
2306 
2318  public function doQuickEditContent(
2319  Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
2320  ) {
2321 
2322  $serialized = $content->serialize( $serialFormat );
2323 
2324  $dbw = wfGetDB( DB_MASTER );
2325  $revision = new Revision( [
2326  'title' => $this->getTitle(), // for determining the default content model
2327  'page' => $this->getId(),
2328  'user_text' => $user->getName(),
2329  'user' => $user->getId(),
2330  'text' => $serialized,
2331  'length' => $content->getSize(),
2332  'comment' => $comment,
2333  'minor_edit' => $minor ? 1 : 0,
2334  ] ); // XXX: set the content object?
2335  $revision->insertOn( $dbw );
2336  $this->updateRevisionOn( $dbw, $revision );
2337 
2338  Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
2339 
2340  }
2341 
2356  public function doUpdateRestrictions( array $limit, array $expiry,
2357  &$cascade, $reason, User $user, $tags = null
2358  ) {
2359  global $wgCascadingRestrictionLevels, $wgContLang;
2360 
2361  if ( wfReadOnly() ) {
2362  return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
2363  }
2364 
2365  $this->loadPageData( 'fromdbmaster' );
2366  $restrictionTypes = $this->mTitle->getRestrictionTypes();
2367  $id = $this->getId();
2368 
2369  if ( !$cascade ) {
2370  $cascade = false;
2371  }
2372 
2373  // Take this opportunity to purge out expired restrictions
2375 
2376  // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
2377  // we expect a single selection, but the schema allows otherwise.
2378  $isProtected = false;
2379  $protect = false;
2380  $changed = false;
2381 
2382  $dbw = wfGetDB( DB_MASTER );
2383 
2384  foreach ( $restrictionTypes as $action ) {
2385  if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2386  $expiry[$action] = 'infinity';
2387  }
2388  if ( !isset( $limit[$action] ) ) {
2389  $limit[$action] = '';
2390  } elseif ( $limit[$action] != '' ) {
2391  $protect = true;
2392  }
2393 
2394  // Get current restrictions on $action
2395  $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2396  if ( $current != '' ) {
2397  $isProtected = true;
2398  }
2399 
2400  if ( $limit[$action] != $current ) {
2401  $changed = true;
2402  } elseif ( $limit[$action] != '' ) {
2403  // Only check expiry change if the action is actually being
2404  // protected, since expiry does nothing on an not-protected
2405  // action.
2406  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2407  $changed = true;
2408  }
2409  }
2410  }
2411 
2412  if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2413  $changed = true;
2414  }
2415 
2416  // If nothing has changed, do nothing
2417  if ( !$changed ) {
2418  return Status::newGood();
2419  }
2420 
2421  if ( !$protect ) { // No protection at all means unprotection
2422  $revCommentMsg = 'unprotectedarticle';
2423  $logAction = 'unprotect';
2424  } elseif ( $isProtected ) {
2425  $revCommentMsg = 'modifiedarticleprotection';
2426  $logAction = 'modify';
2427  } else {
2428  $revCommentMsg = 'protectedarticle';
2429  $logAction = 'protect';
2430  }
2431 
2432  // Truncate for whole multibyte characters
2433  $reason = $wgContLang->truncate( $reason, 255 );
2434 
2435  $logRelationsValues = [];
2436  $logRelationsField = null;
2437  $logParamsDetails = [];
2438 
2439  // Null revision (used for change tag insertion)
2440  $nullRevision = null;
2441 
2442  if ( $id ) { // Protection of existing page
2443  // Avoid PHP 7.1 warning of passing $this by reference
2444  $wikiPage = $this;
2445 
2446  if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2447  return Status::newGood();
2448  }
2449 
2450  // Only certain restrictions can cascade...
2451  $editrestriction = isset( $limit['edit'] )
2452  ? [ $limit['edit'] ]
2453  : $this->mTitle->getRestrictions( 'edit' );
2454  foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2455  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2456  }
2457  foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2458  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2459  }
2460 
2461  $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2462  foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2463  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2464  }
2465  foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2466  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2467  }
2468 
2469  // The schema allows multiple restrictions
2470  if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2471  $cascade = false;
2472  }
2473 
2474  // insert null revision to identify the page protection change as edit summary
2475  $latest = $this->getLatest();
2476  $nullRevision = $this->insertProtectNullRevision(
2477  $revCommentMsg,
2478  $limit,
2479  $expiry,
2480  $cascade,
2481  $reason,
2482  $user
2483  );
2484 
2485  if ( $nullRevision === null ) {
2486  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2487  }
2488 
2489  $logRelationsField = 'pr_id';
2490 
2491  // Update restrictions table
2492  foreach ( $limit as $action => $restrictions ) {
2493  $dbw->delete(
2494  'page_restrictions',
2495  [
2496  'pr_page' => $id,
2497  'pr_type' => $action
2498  ],
2499  __METHOD__
2500  );
2501  if ( $restrictions != '' ) {
2502  $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2503  $dbw->insert(
2504  'page_restrictions',
2505  [
2506  'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
2507  'pr_page' => $id,
2508  'pr_type' => $action,
2509  'pr_level' => $restrictions,
2510  'pr_cascade' => $cascadeValue,
2511  'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2512  ],
2513  __METHOD__
2514  );
2515  $logRelationsValues[] = $dbw->insertId();
2516  $logParamsDetails[] = [
2517  'type' => $action,
2518  'level' => $restrictions,
2519  'expiry' => $expiry[$action],
2520  'cascade' => (bool)$cascadeValue,
2521  ];
2522  }
2523  }
2524 
2525  // Clear out legacy restriction fields
2526  $dbw->update(
2527  'page',
2528  [ 'page_restrictions' => '' ],
2529  [ 'page_id' => $id ],
2530  __METHOD__
2531  );
2532 
2533  // Avoid PHP 7.1 warning of passing $this by reference
2534  $wikiPage = $this;
2535 
2536  Hooks::run( 'NewRevisionFromEditComplete',
2537  [ $this, $nullRevision, $latest, $user ] );
2538  Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2539  } else { // Protection of non-existing page (also known as "title protection")
2540  // Cascade protection is meaningless in this case
2541  $cascade = false;
2542 
2543  if ( $limit['create'] != '' ) {
2544  $dbw->replace( 'protected_titles',
2545  [ [ 'pt_namespace', 'pt_title' ] ],
2546  [
2547  'pt_namespace' => $this->mTitle->getNamespace(),
2548  'pt_title' => $this->mTitle->getDBkey(),
2549  'pt_create_perm' => $limit['create'],
2550  'pt_timestamp' => $dbw->timestamp(),
2551  'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2552  'pt_user' => $user->getId(),
2553  'pt_reason' => $reason,
2554  ], __METHOD__
2555  );
2556  $logParamsDetails[] = [
2557  'type' => 'create',
2558  'level' => $limit['create'],
2559  'expiry' => $expiry['create'],
2560  ];
2561  } else {
2562  $dbw->delete( 'protected_titles',
2563  [
2564  'pt_namespace' => $this->mTitle->getNamespace(),
2565  'pt_title' => $this->mTitle->getDBkey()
2566  ], __METHOD__
2567  );
2568  }
2569  }
2570 
2571  $this->mTitle->flushRestrictions();
2572  InfoAction::invalidateCache( $this->mTitle );
2573 
2574  if ( $logAction == 'unprotect' ) {
2575  $params = [];
2576  } else {
2577  $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2578  $params = [
2579  '4::description' => $protectDescriptionLog, // parameter for IRC
2580  '5:bool:cascade' => $cascade,
2581  'details' => $logParamsDetails, // parameter for localize and api
2582  ];
2583  }
2584 
2585  // Update the protection log
2586  $logEntry = new ManualLogEntry( 'protect', $logAction );
2587  $logEntry->setTarget( $this->mTitle );
2588  $logEntry->setComment( $reason );
2589  $logEntry->setPerformer( $user );
2590  $logEntry->setParameters( $params );
2591  if ( !is_null( $nullRevision ) ) {
2592  $logEntry->setAssociatedRevId( $nullRevision->getId() );
2593  }
2594  $logEntry->setTags( $tags );
2595  if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2596  $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2597  }
2598  $logId = $logEntry->insert();
2599  $logEntry->publish( $logId );
2600 
2601  return Status::newGood( $logId );
2602  }
2603 
2615  public function insertProtectNullRevision( $revCommentMsg, array $limit,
2616  array $expiry, $cascade, $reason, $user = null
2617  ) {
2619  $dbw = wfGetDB( DB_MASTER );
2620 
2621  // Prepare a null revision to be added to the history
2622  $editComment = $wgContLang->ucfirst(
2623  wfMessage(
2624  $revCommentMsg,
2625  $this->mTitle->getPrefixedText()
2626  )->inContentLanguage()->text()
2627  );
2628  if ( $reason ) {
2629  $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2630  }
2631  $protectDescription = $this->protectDescription( $limit, $expiry );
2632  if ( $protectDescription ) {
2633  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2634  $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2635  ->inContentLanguage()->text();
2636  }
2637  if ( $cascade ) {
2638  $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2639  $editComment .= wfMessage( 'brackets' )->params(
2640  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2641  )->inContentLanguage()->text();
2642  }
2643 
2644  $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2645  if ( $nullRev ) {
2646  $nullRev->insertOn( $dbw );
2647 
2648  // Update page record and touch page
2649  $oldLatest = $nullRev->getParentId();
2650  $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2651  }
2652 
2653  return $nullRev;
2654  }
2655 
2660  protected function formatExpiry( $expiry ) {
2662 
2663  if ( $expiry != 'infinity' ) {
2664  return wfMessage(
2665  'protect-expiring',
2666  $wgContLang->timeanddate( $expiry, false, false ),
2667  $wgContLang->date( $expiry, false, false ),
2668  $wgContLang->time( $expiry, false, false )
2669  )->inContentLanguage()->text();
2670  } else {
2671  return wfMessage( 'protect-expiry-indefinite' )
2672  ->inContentLanguage()->text();
2673  }
2674  }
2675 
2683  public function protectDescription( array $limit, array $expiry ) {
2684  $protectDescription = '';
2685 
2686  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2687  # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
2688  # All possible message keys are listed here for easier grepping:
2689  # * restriction-create
2690  # * restriction-edit
2691  # * restriction-move
2692  # * restriction-upload
2693  $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2694  # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ),
2695  # with '' filtered out. All possible message keys are listed below:
2696  # * protect-level-autoconfirmed
2697  # * protect-level-sysop
2698  $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2699  ->inContentLanguage()->text();
2700 
2701  $expiryText = $this->formatExpiry( $expiry[$action] );
2702 
2703  if ( $protectDescription !== '' ) {
2704  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2705  }
2706  $protectDescription .= wfMessage( 'protect-summary-desc' )
2707  ->params( $actionText, $restrictionsText, $expiryText )
2708  ->inContentLanguage()->text();
2709  }
2710 
2711  return $protectDescription;
2712  }
2713 
2725  public function protectDescriptionLog( array $limit, array $expiry ) {
2727 
2728  $protectDescriptionLog = '';
2729 
2730  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2731  $expiryText = $this->formatExpiry( $expiry[$action] );
2732  $protectDescriptionLog .= $wgContLang->getDirMark() .
2733  "[$action=$restrictions] ($expiryText)";
2734  }
2735 
2736  return trim( $protectDescriptionLog );
2737  }
2738 
2748  protected static function flattenRestrictions( $limit ) {
2749  if ( !is_array( $limit ) ) {
2750  throw new MWException( __METHOD__ . ' given non-array restriction set' );
2751  }
2752 
2753  $bits = [];
2754  ksort( $limit );
2755 
2756  foreach ( array_filter( $limit ) as $action => $restrictions ) {
2757  $bits[] = "$action=$restrictions";
2758  }
2759 
2760  return implode( ':', $bits );
2761  }
2762 
2779  public function doDeleteArticle(
2780  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2781  ) {
2782  $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
2783  return $status->isGood();
2784  }
2785 
2803  public function doDeleteArticleReal(
2804  $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2805  ) {
2806  global $wgUser, $wgContentHandlerUseDB;
2807 
2808  wfDebug( __METHOD__ . "\n" );
2809 
2811 
2812  if ( $this->mTitle->getDBkey() === '' ) {
2813  $status->error( 'cannotdelete',
2814  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2815  return $status;
2816  }
2817 
2818  // Avoid PHP 7.1 warning of passing $this by reference
2819  $wikiPage = $this;
2820 
2821  $user = is_null( $user ) ? $wgUser : $user;
2822  if ( !Hooks::run( 'ArticleDelete',
2823  [ &$wikiPage, &$user, &$reason, &$error, &$status, $suppress ]
2824  ) ) {
2825  if ( $status->isOK() ) {
2826  // Hook aborted but didn't set a fatal status
2827  $status->fatal( 'delete-hook-aborted' );
2828  }
2829  return $status;
2830  }
2831 
2832  $dbw = wfGetDB( DB_MASTER );
2833  $dbw->startAtomic( __METHOD__ );
2834 
2835  $this->loadPageData( self::READ_LATEST );
2836  $id = $this->getId();
2837  // T98706: lock the page from various other updates but avoid using
2838  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2839  // the revisions queries (which also JOIN on user). Only lock the page
2840  // row and CAS check on page_latest to see if the trx snapshot matches.
2841  $lockedLatest = $this->lockAndGetLatest();
2842  if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2843  $dbw->endAtomic( __METHOD__ );
2844  // Page not there or trx snapshot is stale
2845  $status->error( 'cannotdelete',
2846  wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2847  return $status;
2848  }
2849 
2850  // At this point we are now comitted to returning an OK
2851  // status unless some DB query error or other exception comes up.
2852  // This way callers don't have to call rollback() if $status is bad
2853  // unless they actually try to catch exceptions (which is rare).
2854 
2855  // we need to remember the old content so we can use it to generate all deletion updates.
2856  $content = $this->getContent( Revision::RAW );
2857 
2858  // Bitfields to further suppress the content
2859  if ( $suppress ) {
2860  $bitfield = 0;
2861  // This should be 15...
2862  $bitfield |= Revision::DELETED_TEXT;
2863  $bitfield |= Revision::DELETED_COMMENT;
2864  $bitfield |= Revision::DELETED_USER;
2865  $bitfield |= Revision::DELETED_RESTRICTED;
2866  } else {
2867  $bitfield = 'rev_deleted';
2868  }
2869 
2883  $row = [
2884  'ar_namespace' => 'page_namespace',
2885  'ar_title' => 'page_title',
2886  'ar_comment' => 'rev_comment',
2887  'ar_user' => 'rev_user',
2888  'ar_user_text' => 'rev_user_text',
2889  'ar_timestamp' => 'rev_timestamp',
2890  'ar_minor_edit' => 'rev_minor_edit',
2891  'ar_rev_id' => 'rev_id',
2892  'ar_parent_id' => 'rev_parent_id',
2893  'ar_text_id' => 'rev_text_id',
2894  'ar_text' => '\'\'', // Be explicit to appease
2895  'ar_flags' => '\'\'', // MySQL's "strict mode"...
2896  'ar_len' => 'rev_len',
2897  'ar_page_id' => 'page_id',
2898  'ar_deleted' => $bitfield,
2899  'ar_sha1' => 'rev_sha1',
2900  ];
2901 
2902  if ( $wgContentHandlerUseDB ) {
2903  $row['ar_content_model'] = 'rev_content_model';
2904  $row['ar_content_format'] = 'rev_content_format';
2905  }
2906 
2907  // Copy all the page revisions into the archive table
2908  $dbw->insertSelect(
2909  'archive',
2910  [ 'page', 'revision' ],
2911  $row,
2912  [
2913  'page_id' => $id,
2914  'page_id = rev_page'
2915  ],
2916  __METHOD__
2917  );
2918 
2919  // Now that it's safely backed up, delete it
2920  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2921 
2922  if ( !$dbw->cascadingDeletes() ) {
2923  $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
2924  }
2925 
2926  // Clone the title, so we have the information we need when we log
2927  $logTitle = clone $this->mTitle;
2928 
2929  // Log the deletion, if the page was suppressed, put it in the suppression log instead
2930  $logtype = $suppress ? 'suppress' : 'delete';
2931 
2932  $logEntry = new ManualLogEntry( $logtype, 'delete' );
2933  $logEntry->setPerformer( $user );
2934  $logEntry->setTarget( $logTitle );
2935  $logEntry->setComment( $reason );
2936  $logid = $logEntry->insert();
2937 
2938  $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) {
2939  // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
2940  $logEntry->publish( $logid );
2941  } );
2942 
2943  $dbw->endAtomic( __METHOD__ );
2944 
2945  $this->doDeleteUpdates( $id, $content );
2946 
2947  // Avoid PHP 7.1 warning of passing $this by reference
2948  $page = $this;
2949  Hooks::run( 'ArticleDeleteComplete',
2950  [ &$page, &$user, $reason, $id, $content, $logEntry ] );
2951  $status->value = $logid;
2952 
2953  // Show log excerpt on 404 pages rather than just a link
2955  $key = wfMemcKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2956  $cache->set( $key, 1, $cache::TTL_DAY );
2957 
2958  return $status;
2959  }
2960 
2967  public function lockAndGetLatest() {
2968  return (int)wfGetDB( DB_MASTER )->selectField(
2969  'page',
2970  'page_latest',
2971  [
2972  'page_id' => $this->getId(),
2973  // Typically page_id is enough, but some code might try to do
2974  // updates assuming the title is the same, so verify that
2975  'page_namespace' => $this->getTitle()->getNamespace(),
2976  'page_title' => $this->getTitle()->getDBkey()
2977  ],
2978  __METHOD__,
2979  [ 'FOR UPDATE' ]
2980  );
2981  }
2982 
2991  public function doDeleteUpdates( $id, Content $content = null ) {
2992  // Update site status
2993  DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
2994 
2995  // Delete pagelinks, update secondary indexes, etc
2996  $updates = $this->getDeletionUpdates( $content );
2997  foreach ( $updates as $update ) {
2998  DeferredUpdates::addUpdate( $update );
2999  }
3000 
3001  // Reparse any pages transcluding this page
3002  LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
3003 
3004  // Reparse any pages including this image
3005  if ( $this->mTitle->getNamespace() == NS_FILE ) {
3006  LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
3007  }
3008 
3009  // Clear caches
3010  WikiPage::onArticleDelete( $this->mTitle );
3011 
3012  // Reset this object and the Title object
3013  $this->loadFromRow( false, self::READ_LATEST );
3014 
3015  // Search engine
3016  DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3017  }
3018 
3047  public function doRollback(
3048  $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3049  ) {
3050  $resultDetails = null;
3051 
3052  // Check permissions
3053  $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3054  $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3055  $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3056 
3057  if ( !$user->matchEditToken( $token, [ $this->mTitle->getPrefixedText(), $fromP ] ) ) {
3058  $errors[] = [ 'sessionfailure' ];
3059  }
3060 
3061  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3062  $errors[] = [ 'actionthrottledtext' ];
3063  }
3064 
3065  // If there were errors, bail out now
3066  if ( !empty( $errors ) ) {
3067  return $errors;
3068  }
3069 
3070  return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3071  }
3072 
3093  public function commitRollback( $fromP, $summary, $bot,
3094  &$resultDetails, User $guser, $tags = null
3095  ) {
3096  global $wgUseRCPatrol, $wgContLang;
3097 
3098  $dbw = wfGetDB( DB_MASTER );
3099 
3100  if ( wfReadOnly() ) {
3101  return [ [ 'readonlytext' ] ];
3102  }
3103 
3104  // Get the last editor
3105  $current = $this->getRevision();
3106  if ( is_null( $current ) ) {
3107  // Something wrong... no page?
3108  return [ [ 'notanarticle' ] ];
3109  }
3110 
3111  $from = str_replace( '_', ' ', $fromP );
3112  // User name given should match up with the top revision.
3113  // If the user was deleted then $from should be empty.
3114  if ( $from != $current->getUserText() ) {
3115  $resultDetails = [ 'current' => $current ];
3116  return [ [ 'alreadyrolled',
3117  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3118  htmlspecialchars( $fromP ),
3119  htmlspecialchars( $current->getUserText() )
3120  ] ];
3121  }
3122 
3123  // Get the last edit not by this person...
3124  // Note: these may not be public values
3125  $user = intval( $current->getUser( Revision::RAW ) );
3126  $user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
3127  $s = $dbw->selectRow( 'revision',
3128  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3129  [ 'rev_page' => $current->getPage(),
3130  "rev_user != {$user} OR rev_user_text != {$user_text}"
3131  ], __METHOD__,
3132  [ 'USE INDEX' => 'page_timestamp',
3133  'ORDER BY' => 'rev_timestamp DESC' ]
3134  );
3135  if ( $s === false ) {
3136  // No one else ever edited this page
3137  return [ [ 'cantrollback' ] ];
3138  } elseif ( $s->rev_deleted & Revision::DELETED_TEXT
3139  || $s->rev_deleted & Revision::DELETED_USER
3140  ) {
3141  // Only admins can see this text
3142  return [ [ 'notvisiblerev' ] ];
3143  }
3144 
3145  // Generate the edit summary if necessary
3146  $target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST );
3147  if ( empty( $summary ) ) {
3148  if ( $from == '' ) { // no public user name
3149  $summary = wfMessage( 'revertpage-nouser' );
3150  } else {
3151  $summary = wfMessage( 'revertpage' );
3152  }
3153  }
3154 
3155  // Allow the custom summary to use the same args as the default message
3156  $args = [
3157  $target->getUserText(), $from, $s->rev_id,
3158  $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3159  $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
3160  ];
3161  if ( $summary instanceof Message ) {
3162  $summary = $summary->params( $args )->inContentLanguage()->text();
3163  } else {
3165  }
3166 
3167  // Trim spaces on user supplied text
3168  $summary = trim( $summary );
3169 
3170  // Truncate for whole multibyte characters.
3171  $summary = $wgContLang->truncate( $summary, 255 );
3172 
3173  // Save
3174  $flags = EDIT_UPDATE;
3175 
3176  if ( $guser->isAllowed( 'minoredit' ) ) {
3177  $flags |= EDIT_MINOR;
3178  }
3179 
3180  if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3182  }
3183 
3184  // Actually store the edit
3185  $status = $this->doEditContent(
3186  $target->getContent(),
3187  $summary,
3188  $flags,
3189  $target->getId(),
3190  $guser,
3191  null,
3192  $tags
3193  );
3194 
3195  // Set patrolling and bot flag on the edits, which gets rollbacked.
3196  // This is done even on edit failure to have patrolling in that case (bug 62157).
3197  $set = [];
3198  if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3199  // Mark all reverted edits as bot
3200  $set['rc_bot'] = 1;
3201  }
3202 
3203  if ( $wgUseRCPatrol ) {
3204  // Mark all reverted edits as patrolled
3205  $set['rc_patrolled'] = 1;
3206  }
3207 
3208  if ( count( $set ) ) {
3209  $dbw->update( 'recentchanges', $set,
3210  [ /* WHERE */
3211  'rc_cur_id' => $current->getPage(),
3212  'rc_user_text' => $current->getUserText(),
3213  'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3214  ],
3215  __METHOD__
3216  );
3217  }
3218 
3219  if ( !$status->isOK() ) {
3220  return $status->getErrorsArray();
3221  }
3222 
3223  // raise error, when the edit is an edit without a new version
3224  $statusRev = isset( $status->value['revision'] )
3225  ? $status->value['revision']
3226  : null;
3227  if ( !( $statusRev instanceof Revision ) ) {
3228  $resultDetails = [ 'current' => $current ];
3229  return [ [ 'alreadyrolled',
3230  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3231  htmlspecialchars( $fromP ),
3232  htmlspecialchars( $current->getUserText() )
3233  ] ];
3234  }
3235 
3236  $revId = $statusRev->getId();
3237 
3238  Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3239 
3240  $resultDetails = [
3241  'summary' => $summary,
3242  'current' => $current,
3243  'target' => $target,
3244  'newid' => $revId
3245  ];
3246 
3247  return [];
3248  }
3249 
3261  public static function onArticleCreate( Title $title ) {
3262  // Update existence markers on article/talk tabs...
3263  $other = $title->getOtherPage();
3264 
3265  $other->purgeSquid();
3266 
3267  $title->touchLinks();
3268  $title->purgeSquid();
3269  $title->deleteTitleProtection();
3270  }
3271 
3277  public static function onArticleDelete( Title $title ) {
3279 
3280  // Update existence markers on article/talk tabs...
3281  $other = $title->getOtherPage();
3282 
3283  $other->purgeSquid();
3284 
3285  $title->touchLinks();
3286  $title->purgeSquid();
3287 
3288  // File cache
3290  InfoAction::invalidateCache( $title );
3291 
3292  // Messages
3293  if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3294  MessageCache::singleton()->replace( $title->getDBkey(), false );
3295 
3296  if ( $wgContLang->hasVariants() ) {
3297  $wgContLang->updateConversionTable( $title );
3298  }
3299  }
3300 
3301  // Images
3302  if ( $title->getNamespace() == NS_FILE ) {
3303  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
3304  }
3305 
3306  // User talk pages
3307  if ( $title->getNamespace() == NS_USER_TALK ) {
3308  $user = User::newFromName( $title->getText(), false );
3309  if ( $user ) {
3310  $user->setNewtalk( false );
3311  }
3312  }
3313 
3314  // Image redirects
3315  RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3316  }
3317 
3324  public static function onArticleEdit( Title $title, Revision $revision = null ) {
3325  // Invalidate caches of articles which include this page
3326  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
3327 
3328  // Invalidate the caches of all pages which redirect here
3329  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
3330 
3331  // Purge CDN for this page only
3332  $title->purgeSquid();
3333  // Clear file cache for this page only
3335 
3336  $revid = $revision ? $revision->getId() : null;
3337  DeferredUpdates::addCallableUpdate( function() use ( $title, $revid ) {
3338  InfoAction::invalidateCache( $title, $revid );
3339  } );
3340  }
3341 
3350  public function getCategories() {
3351  $id = $this->getId();
3352  if ( $id == 0 ) {
3353  return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3354  }
3355 
3356  $dbr = wfGetDB( DB_SLAVE );
3357  $res = $dbr->select( 'categorylinks',
3358  [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3359  // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
3360  // as not being aliases, and NS_CATEGORY is numeric
3361  [ 'cl_from' => $id ],
3362  __METHOD__ );
3363 
3364  return TitleArray::newFromResult( $res );
3365  }
3366 
3373  public function getHiddenCategories() {
3374  $result = [];
3375  $id = $this->getId();
3376 
3377  if ( $id == 0 ) {
3378  return [];
3379  }
3380 
3381  $dbr = wfGetDB( DB_SLAVE );
3382  $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3383  [ 'cl_to' ],
3384  [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3385  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3386  __METHOD__ );
3387 
3388  if ( $res !== false ) {
3389  foreach ( $res as $row ) {
3390  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3391  }
3392  }
3393 
3394  return $result;
3395  }
3396 
3406  public static function getAutosummary( $oldtext, $newtext, $flags ) {
3407  // NOTE: stub for backwards-compatibility. assumes the given text is
3408  // wikitext. will break horribly if it isn't.
3409 
3410  ContentHandler::deprecated( __METHOD__, '1.21' );
3411 
3413  $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
3414  $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
3415 
3416  return $handler->getAutosummary( $oldContent, $newContent, $flags );
3417  }
3418 
3426  public function getAutoDeleteReason( &$hasHistory ) {
3427  return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3428  }
3429 
3437  public function updateCategoryCounts( array $added, array $deleted ) {
3438  $that = $this;
3439  $method = __METHOD__;
3440  $dbw = wfGetDB( DB_MASTER );
3441 
3442  // Do this at the end of the commit to reduce lock wait timeouts
3443  $dbw->onTransactionPreCommitOrIdle(
3444  function () use ( $dbw, $that, $method, $added, $deleted ) {
3445  $ns = $that->getTitle()->getNamespace();
3446 
3447  $addFields = [ 'cat_pages = cat_pages + 1' ];
3448  $removeFields = [ 'cat_pages = cat_pages - 1' ];
3449  if ( $ns == NS_CATEGORY ) {
3450  $addFields[] = 'cat_subcats = cat_subcats + 1';
3451  $removeFields[] = 'cat_subcats = cat_subcats - 1';
3452  } elseif ( $ns == NS_FILE ) {
3453  $addFields[] = 'cat_files = cat_files + 1';
3454  $removeFields[] = 'cat_files = cat_files - 1';
3455  }
3456 
3457  if ( count( $added ) ) {
3458  $existingAdded = $dbw->selectFieldValues(
3459  'category',
3460  'cat_title',
3461  [ 'cat_title' => $added ],
3462  __METHOD__
3463  );
3464 
3465  // For category rows that already exist, do a plain
3466  // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3467  // to avoid creating gaps in the cat_id sequence.
3468  if ( count( $existingAdded ) ) {
3469  $dbw->update(
3470  'category',
3471  $addFields,
3472  [ 'cat_title' => $existingAdded ],
3473  __METHOD__
3474  );
3475  }
3476 
3477  $missingAdded = array_diff( $added, $existingAdded );
3478  if ( count( $missingAdded ) ) {
3479  $insertRows = [];
3480  foreach ( $missingAdded as $cat ) {
3481  $insertRows[] = [
3482  'cat_title' => $cat,
3483  'cat_pages' => 1,
3484  'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
3485  'cat_files' => ( $ns == NS_FILE ) ? 1 : 0,
3486  ];
3487  }
3488  $dbw->upsert(
3489  'category',
3490  $insertRows,
3491  [ 'cat_title' ],
3492  $addFields,
3493  $method
3494  );
3495  }
3496  }
3497 
3498  if ( count( $deleted ) ) {
3499  $dbw->update(
3500  'category',
3501  $removeFields,
3502  [ 'cat_title' => $deleted ],
3503  $method
3504  );
3505  }
3506 
3507  foreach ( $added as $catName ) {
3508  $cat = Category::newFromName( $catName );
3509  Hooks::run( 'CategoryAfterPageAdded', [ $cat, $that ] );
3510  }
3511 
3512  foreach ( $deleted as $catName ) {
3513  $cat = Category::newFromName( $catName );
3514  Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $that ] );
3515  }
3516  }
3517  );
3518  }
3519 
3527  if ( wfReadOnly() ) {
3528  return;
3529  }
3530 
3531  if ( !Hooks::run( 'OpportunisticLinksUpdate',
3532  [ $this, $this->mTitle, $parserOutput ]
3533  ) ) {
3534  return;
3535  }
3536 
3537  $config = RequestContext::getMain()->getConfig();
3538 
3539  $params = [
3540  'isOpportunistic' => true,
3541  'rootJobTimestamp' => $parserOutput->getCacheTime()
3542  ];
3543 
3544  if ( $this->mTitle->areRestrictionsCascading() ) {
3545  // If the page is cascade protecting, the links should really be up-to-date
3546  JobQueueGroup::singleton()->lazyPush(
3547  RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3548  );
3549  } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3550  // Assume the output contains "dynamic" time/random based magic words.
3551  // Only update pages that expired due to dynamic content and NOT due to edits
3552  // to referenced templates/files. When the cache expires due to dynamic content,
3553  // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3554  // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3555  // template/file edit already triggered recursive RefreshLinksJob jobs.
3556  if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3557  // If a page is uncacheable, do not keep spamming a job for it.
3558  // Although it would be de-duplicated, it would still waste I/O.
3560  $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3561  $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3562  if ( $cache->add( $key, time(), $ttl ) ) {
3563  JobQueueGroup::singleton()->lazyPush(
3564  RefreshLinksJob::newDynamic( $this->mTitle, $params )
3565  );
3566  }
3567  }
3568  }
3569  }
3570 
3580  public function getDeletionUpdates( Content $content = null ) {
3581  if ( !$content ) {
3582  // load content object, which may be used to determine the necessary updates.
3583  // XXX: the content may not be needed to determine the updates.
3584  $content = $this->getContent( Revision::RAW );
3585  }
3586 
3587  if ( !$content ) {
3588  $updates = [];
3589  } else {
3590  $updates = $content->getDeletionUpdates( $this );
3591  }
3592 
3593  Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3594  return $updates;
3595  }
3596 }
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:568
getLinksTimestamp()
Get the page_links_updated field.
Definition: WikiPage.php:530
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:804
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:99
static newFromRow($row)
Make a Title object from a DB row.
Definition: Title.php:465
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3066
setLastEdit(Revision $revision)
Set the latest revision.
Definition: WikiPage.php:627
makeParserOptions($context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1982
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:3261
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:1349
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4450
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1353
string $mLinksUpdated
Definition: WikiPage.php:81
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.
static newFromName($name)
Factory function.
Definition: Category.php:111
the array() calling protocol came about after MediaWiki 1.4rc1.
replaceSectionContent($sectionId, Content $sectionContent, $sectionTitle= '', $edittime=null)
Definition: WikiPage.php:1382
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:278
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2325
getLatest()
Get the page_latest field.
Definition: WikiPage.php:541
stdClass $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition: WikiPage.php:46
$context
Definition: load.php:44
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
serialize($format=null)
Convenience method for serializing this Content object.
string $mTouched
Definition: WikiPage.php:76
getUser($audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:717
getParserOutput(ParserOptions $parserOptions, $oldid=null)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1058
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:893
clearNotification(&$title, $oldid=0)
Clear the user's notification timestamp for the given title.
Definition: User.php:3532
doEditContent(Content $content, $summary, $flags=0, $baseRevId=false, User $user=null, $serialFormat=null, $tags=null)
Change an existing article or create a new article.
Definition: WikiPage.php:1591
int $mId
Definition: WikiPage.php:51
static checkCache(Title $title, Content $content, User $user)
Check that a prepared edit is in cache and still up-to-date.
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:56
getTimestamp()
Definition: Revision.php:1152
See docs/deferred.txt.
Definition: LinksUpdate.php:28
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3391
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:2683
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:1936
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:1802
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:887
pingLimiter($action= 'edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1777
Title $mTitle
Definition: WikiPage.php:35
updateCategoryCounts(array $added, array $deleted)
Update all the appropriate counts in the category table, given that we've added the categories $added...
Definition: WikiPage.php:3437
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
updateRevisionOn($dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1206
clear()
Clear the object.
Definition: WikiPage.php:225
Handles purging appropriate CDN URLs given a title (or titles)
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
Definition: WikiPage.php:3526
$comment
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
$source
const DBO_TRX
Definition: Defines.php:33
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition: WikiPage.php:787
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:1008
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:1326
static getLocalClusterInstance()
Get the main cluster-local cache object.
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2552
getContributors()
Get a list of users who have edited this article, not including the user who made the most recent rev...
Definition: WikiPage.php:984
checkFlags($flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: WikiPage.php:1464
const EDIT_MINOR
Definition: Defines.php:182
static getMainStashInstance()
Get the cache object for the main stash.
const EDIT_UPDATE
Definition: Defines.php:181
doQuickEditContent(Content $content, User $user, $comment= '', $minor=false, $serialFormat=null)
Edit an article without doing all that other stuff The article must already exist; link tables etc ar...
Definition: WikiPage.php:2318
insertProtectNullRevision($revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
Definition: WikiPage.php:2615
Represents a title within MediaWiki.
Definition: Title.php:34
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:148
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:519
static newFatal($message)
Factory function for fatal errors.
Definition: Status.php:89
matchEditToken($val, $salt= '', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4224
getDeletionUpdates(Content $content=null)
Returns a list of updates to be performed when this page is deleted.
Definition: WikiPage.php:3580
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition: WikiPage.php:71
magic word & $parser
Definition: hooks.txt:2325
clearCacheFields()
Clear the object cache fields.
Definition: WikiPage.php:236
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:590
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2095
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist & $tables
Definition: hooks.txt:969
pageDataFromTitle($dbr, $title, $options=[])
Fetch a page record matching the Title object's namespace and title using a sanitized title string...
Definition: WikiPage.php:325
getActionOverrides()
Returns overrides for action handlers.
Definition: WikiPage.php:195
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:469
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':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:1800
getTitleKey()
Get the user's name escaped by underscores.
Definition: User.php:2131
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:1528
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:487
static addCallableUpdate($callable, $type=self::POSTSEND)
Add a callable update.
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:508
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:41
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:395
static getLocalizedName($name, Language $lang=null)
Returns the localized name for a given content model.
getRevision()
Get the latest revision.
Definition: WikiPage.php:636
getContent($audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:657
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:67
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:107
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:2645
static getMain()
Static methods.
static singleton()
Get an instance of this class.
Definition: LinkCache.php:61
const FOR_PUBLIC
Definition: Revision.php:83
const EDIT_FORCE_BOT
Definition: Defines.php:184
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:3093
Revision $mLastRevision
Definition: WikiPage.php:66
static deprecated($func, $version, $component=false)
Logs a deprecation warning, visible if $wgDevelopmentWarnings, but only if self::$enableDeprecationWa...
isAllowed($action= '')
Internal mechanics of testing a permission.
Definition: User.php:3421
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:911
wfWarn($msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
getComment($audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:773
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:1008
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:1008
static runLegacyHooks($event, $args=[], $warn=null)
Call a legacy hook that uses text instead of Content objects.
followRedirect()
Get the Title object or URL this page redirects to.
Definition: WikiPage.php:935
const NS_MEDIA
Definition: Defines.php:58
$res
Definition: database.txt:21
static loadFromTimestamp($db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:290
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:1133
$summary
doDeleteArticle($reason, $suppress=false, $u1=null, $u2=null, &$error= '', User $user=null)
Same as doDeleteArticleReal(), but returns a simple boolean.
Definition: WikiPage.php:2779
static newNullRevision($dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
Definition: Revision.php:1626
static newFromRow($row, $from= 'fromdb')
Constructor from a database row.
Definition: WikiPage.php:159
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:3047
Class for handling updates to the site_stats table.
incEditCount()
Deferred version of incEditCountImmediate()
Definition: User.php:4883
doCreate(Content $content, $flags, User $user, $summary, array $meta)
Definition: WikiPage.php:1857
const EDIT_AUTOSUMMARY
Definition: Defines.php:186
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
doModify(Content $content, $flags, User $user, $summary, array $meta)
Definition: WikiPage.php:1690
Base interface for content objects.
Definition: Content.php:34
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 just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing 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
getCacheTime()
Definition: CacheTime.php:50
getOldestRevision()
Get the Revision object of the oldest revision.
Definition: WikiPage.php:552
$cache
Definition: mcc.php:33
$params
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
Definition: WikiPage.php:3373
doViewUpdates(User $user, $oldid=0)
Do standard deferred updates after page view (existing or missing page)
Definition: WikiPage.php:1089
getTitle()
Get the title object of the article.
Definition: WikiPage.php:217
const NS_CATEGORY
Definition: Defines.php:84
const EDIT_SUPPRESS_RC
Definition: Defines.php:183
static selectFields()
Return the list of revision fields that should be selected to create a new revision.
Definition: Revision.php:429
static isIP($name)
Does the string match an anonymous IPv4 address?
Definition: User.php:830
wfIncrStats($key, $count=1)
Increment a statistics counter.
const DELETED_RESTRICTED
Definition: Revision.php:79
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:2342
const DB_SLAVE
Definition: Defines.php:47
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:916
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for...
Definition: WikiPage.php:460
static addUpdate(DeferrableUpdate $update, $type=self::POSTSEND)
Add an update to the deferred list.
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:934
static getDBOptions($bitfield)
Get an appropriate DB index and options 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:4254
const NS_FILE
Definition: Defines.php:76
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:1588
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:821
const RAW
Definition: Revision.php:85
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:946
const NS_MEDIAWIKI
Definition: Defines.php:78
getContentHandler()
Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
Definition: WikiPage.php:209
doPurge()
Perform the actions of a page purging.
Definition: WikiPage.php:1108
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:246
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
Definition: WikiPage.php:3426
static onArticleEdit(Title $title, Revision $revision=null)
Purge caches on page update etc.
Definition: WikiPage.php:3324
doDeleteUpdates($id, Content $content=null)
Do some database updates after deletion.
Definition: WikiPage.php:2991
const DELETED_TEXT
Definition: Revision.php:76
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.
const TS_MW
MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
getUserText($audience=Revision::FOR_PUBLIC, User $user=null)
Definition: WikiPage.php:755
Class representing a MediaWiki article and history.
Definition: WikiPage.php:29
static newFromId($id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:99
replaceSectionAtRev($sectionId, Content $sectionContent, $sectionTitle= '', $baseRevId=null)
Definition: WikiPage.php:1421
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Definition: WikiPage.php:2025
$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)
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:78
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:3350
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:2157
getId()
Get the user's ID.
Definition: User.php:2070
insertOn($dbw)
Insert a new revision into the database, returning the new revision ID number on success and dies hor...
Definition: Revision.php:1354
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
const EDIT_NEW
Definition: Defines.php:180
getTimestamp()
Definition: WikiPage.php:690
insertOn($dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1162
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:1008
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3277
static convertSelectType($type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition: WikiPage.php:171
supportsSections()
Returns true if this page's content model supports sections.
Definition: WikiPage.php:1364
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:1029
updateRedirectOn($dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
Definition: WikiPage.php:1277
$mDataLoaded
Definition: WikiPage.php:40
static newFromRow($row)
Definition: Revision.php:219
shouldCheckParserCache(ParserOptions $parserOptions, $oldId)
Should the parser cache be used?
Definition: WikiPage.php:1038
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:848
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:1008
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:1312
static newFromID($id, $from= 'fromdb')
Constructor from a page id.
Definition: WikiPage.php:132
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:1008
loadPageData($from= 'fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:355
insertRedirectEntry(Title $rt, $oldLatest=null)
Insert or update the redirect table entry for this page to indicate it redirects to $rt...
Definition: WikiPage.php:909
wfMemcKey()
Make a cache key for the local wiki.
const DB_MASTER
Definition: Defines.php:48
setTimestamp($ts)
Set the page timestamp (use only to avoid DB queries)
Definition: WikiPage.php:704
doDeleteArticleReal($reason, $suppress=false, $u1=null, $u2=null, &$error= '', User $user=null)
Back-end article deletion Deletes the article with database consistency, writes logs, purges caches.
Definition: WikiPage.php:2803
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2967
const DELETED_COMMENT
Definition: Revision.php:77
foreach($res as $row) $serialized
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:301
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:766
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).
prepareTextForEdit($text, $revid=null, User $user=null)
Prepare text which is about to be saved.
Definition: WikiPage.php:2004
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:266
static getAutosummary($oldtext, $newtext, $flags)
Return an applicable autosummary if one exists for the given edit.
Definition: WikiPage.php:3406
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition: WikiPage.php:256
const NS_USER_TALK
Definition: Defines.php:73
formatExpiry($expiry)
Definition: WikiPage.php:2660
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:246
Title $mRedirectTarget
Definition: WikiPage.php:61
__construct(Title $title)
Constructor and clear the article.
Definition: WikiPage.php:87
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:2342
getText($audience=Revision::FOR_PUBLIC, User $user=null)
Get the text of the current revision.
Definition: WikiPage.php:677
pageDataFromId($dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition: WikiPage.php:339
Special handling for category pages.
static & makeTitle($ns, $title, $fragment= '', $interwiki= '')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:524
static singleton()
Get the signleton instance of this class.
purgeSquid()
Purge all applicable CDN URLs.
Definition: Title.php:3621
static newGood($value=null)
Factory function for good results.
Definition: Status.php:101
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:2356
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:2748
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:2342
$wgUser
Definition: Setup.php:794
getCreator($audience=Revision::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition: WikiPage.php:736
protectDescriptionLog(array $limit, array $expiry)
Builds the description to serve as comment for the log entry.
Definition: WikiPage.php:2725