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