MediaWiki REL1_32
WikiPage.php
Go to the documentation of this file.
1<?php
33use Wikimedia\Assert\Assert;
37
44class WikiPage implements Page, IDBAccessObject {
45 // Constants for $mDataLoadedFrom and related
46
50 public $mTitle = null;
51
55 public $mDataLoaded = false; // !< Boolean
56 public $mIsRedirect = false; // !< Boolean
57 public $mLatest = false; // !< Integer (false means "not loaded")
61 public $mPreparedEdit = false;
62
66 protected $mId = null;
67
71 protected $mDataLoadedFrom = self::READ_NONE;
72
76 protected $mRedirectTarget = null;
77
81 protected $mLastRevision = null;
82
86 protected $mTimestamp = '';
87
91 protected $mTouched = '19700101000000';
92
96 protected $mLinksUpdated = '19700101000000';
97
101 private $derivedDataUpdater = null;
102
107 public function __construct( Title $title ) {
108 $this->mTitle = $title;
109 }
110
115 public function __clone() {
116 $this->mTitle = clone $this->mTitle;
117 }
118
127 public static function factory( Title $title ) {
128 $ns = $title->getNamespace();
129
130 if ( $ns == NS_MEDIA ) {
131 throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
132 } elseif ( $ns < 0 ) {
133 throw new MWException( "Invalid or virtual namespace $ns given." );
134 }
135
136 $page = null;
137 if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
138 return $page;
139 }
140
141 switch ( $ns ) {
142 case NS_FILE:
143 $page = new WikiFilePage( $title );
144 break;
145 case NS_CATEGORY:
146 $page = new WikiCategoryPage( $title );
147 break;
148 default:
149 $page = new WikiPage( $title );
150 }
151
152 return $page;
153 }
154
165 public static function newFromID( $id, $from = 'fromdb' ) {
166 // page ids are never 0 or negative, see T63166
167 if ( $id < 1 ) {
168 return null;
169 }
170
171 $from = self::convertSelectType( $from );
172 $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
173 $pageQuery = self::getQueryInfo();
174 $row = $db->selectRow(
175 $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
176 [], $pageQuery['joins']
177 );
178 if ( !$row ) {
179 return null;
180 }
181 return self::newFromRow( $row, $from );
182 }
183
195 public static function newFromRow( $row, $from = 'fromdb' ) {
196 $page = self::factory( Title::newFromRow( $row ) );
197 $page->loadFromRow( $row, $from );
198 return $page;
199 }
200
207 protected static function convertSelectType( $type ) {
208 switch ( $type ) {
209 case 'fromdb':
210 return self::READ_NORMAL;
211 case 'fromdbmaster':
212 return self::READ_LATEST;
213 case 'forupdate':
214 return self::READ_LOCKING;
215 default:
216 // It may already be an integer or whatever else
217 return $type;
218 }
219 }
220
224 private function getRevisionStore() {
225 return MediaWikiServices::getInstance()->getRevisionStore();
226 }
227
231 private function getRevisionRenderer() {
232 return MediaWikiServices::getInstance()->getRevisionRenderer();
233 }
234
238 private function getParserCache() {
239 return MediaWikiServices::getInstance()->getParserCache();
240 }
241
245 private function getDBLoadBalancer() {
246 return MediaWikiServices::getInstance()->getDBLoadBalancer();
247 }
248
255 public function getActionOverrides() {
256 return $this->getContentHandler()->getActionOverrides();
257 }
258
268 public function getContentHandler() {
269 return ContentHandler::getForModelID( $this->getContentModel() );
270 }
271
276 public function getTitle() {
277 return $this->mTitle;
278 }
279
284 public function clear() {
285 $this->mDataLoaded = false;
286 $this->mDataLoadedFrom = self::READ_NONE;
287
288 $this->clearCacheFields();
289 }
290
295 protected function clearCacheFields() {
296 $this->mId = null;
297 $this->mRedirectTarget = null; // Title object if set
298 $this->mLastRevision = null; // Latest revision
299 $this->mTouched = '19700101000000';
300 $this->mLinksUpdated = '19700101000000';
301 $this->mTimestamp = '';
302 $this->mIsRedirect = false;
303 $this->mLatest = false;
304 // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
305 // checks the requested rev ID and content against the cached one. For most
306 // content types, the output should not change during the lifetime of this cache.
307 // Clearing it can cause extra parses on edit for no reason.
308 }
309
315 public function clearPreparedEdit() {
316 $this->mPreparedEdit = false;
317 }
318
326 public static function selectFields() {
328
329 wfDeprecated( __METHOD__, '1.31' );
330
331 $fields = [
332 'page_id',
333 'page_namespace',
334 'page_title',
335 'page_restrictions',
336 'page_is_redirect',
337 'page_is_new',
338 'page_random',
339 'page_touched',
340 'page_links_updated',
341 'page_latest',
342 'page_len',
343 ];
344
346 $fields[] = 'page_content_model';
347 }
348
349 if ( $wgPageLanguageUseDB ) {
350 $fields[] = 'page_lang';
351 }
352
353 return $fields;
354 }
355
365 public static function getQueryInfo() {
367
368 $ret = [
369 'tables' => [ 'page' ],
370 'fields' => [
371 'page_id',
372 'page_namespace',
373 'page_title',
374 'page_restrictions',
375 'page_is_redirect',
376 'page_is_new',
377 'page_random',
378 'page_touched',
379 'page_links_updated',
380 'page_latest',
381 'page_len',
382 ],
383 'joins' => [],
384 ];
385
387 $ret['fields'][] = 'page_content_model';
388 }
389
390 if ( $wgPageLanguageUseDB ) {
391 $ret['fields'][] = 'page_lang';
392 }
393
394 return $ret;
395 }
396
404 protected function pageData( $dbr, $conditions, $options = [] ) {
405 $pageQuery = self::getQueryInfo();
406
407 // Avoid PHP 7.1 warning of passing $this by reference
408 $wikiPage = $this;
409
410 Hooks::run( 'ArticlePageDataBefore', [
411 &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
412 ] );
413
414 $row = $dbr->selectRow(
415 $pageQuery['tables'],
416 $pageQuery['fields'],
417 $conditions,
418 __METHOD__,
419 $options,
420 $pageQuery['joins']
421 );
422
423 Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
424
425 return $row;
426 }
427
437 public function pageDataFromTitle( $dbr, $title, $options = [] ) {
438 return $this->pageData( $dbr, [
439 'page_namespace' => $title->getNamespace(),
440 'page_title' => $title->getDBkey() ], $options );
441 }
442
451 public function pageDataFromId( $dbr, $id, $options = [] ) {
452 return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
453 }
454
467 public function loadPageData( $from = 'fromdb' ) {
468 $from = self::convertSelectType( $from );
469 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
470 // We already have the data from the correct location, no need to load it twice.
471 return;
472 }
473
474 if ( is_int( $from ) ) {
475 list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
476 $loadBalancer = $this->getDBLoadBalancer();
477 $db = $loadBalancer->getConnection( $index );
478 $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
479
480 if ( !$data
481 && $index == DB_REPLICA
482 && $loadBalancer->getServerCount() > 1
483 && $loadBalancer->hasOrMadeRecentMasterChanges()
484 ) {
485 $from = self::READ_LATEST;
486 list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
487 $db = $loadBalancer->getConnection( $index );
488 $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
489 }
490 } else {
491 // No idea from where the caller got this data, assume replica DB.
492 $data = $from;
493 $from = self::READ_NORMAL;
494 }
495
496 $this->loadFromRow( $data, $from );
497 }
498
512 public function wasLoadedFrom( $from ) {
513 $from = self::convertSelectType( $from );
514
515 if ( !is_int( $from ) ) {
516 // No idea from where the caller got this data, assume replica DB.
517 $from = self::READ_NORMAL;
518 }
519
520 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
521 return true;
522 }
523
524 return false;
525 }
526
538 public function loadFromRow( $data, $from ) {
539 $lc = MediaWikiServices::getInstance()->getLinkCache();
540 $lc->clearLink( $this->mTitle );
541
542 if ( $data ) {
543 $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
544
545 $this->mTitle->loadFromRow( $data );
546
547 // Old-fashioned restrictions
548 $this->mTitle->loadRestrictions( $data->page_restrictions );
549
550 $this->mId = intval( $data->page_id );
551 $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
552 $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
553 $this->mIsRedirect = intval( $data->page_is_redirect );
554 $this->mLatest = intval( $data->page_latest );
555 // T39225: $latest may no longer match the cached latest Revision object.
556 // Double-check the ID of any cached latest Revision object for consistency.
557 if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
558 $this->mLastRevision = null;
559 $this->mTimestamp = '';
560 }
561 } else {
562 $lc->addBadLinkObj( $this->mTitle );
563
564 $this->mTitle->loadFromRow( false );
565
566 $this->clearCacheFields();
567
568 $this->mId = 0;
569 }
570
571 $this->mDataLoaded = true;
572 $this->mDataLoadedFrom = self::convertSelectType( $from );
573 }
574
578 public function getId() {
579 if ( !$this->mDataLoaded ) {
580 $this->loadPageData();
581 }
582 return $this->mId;
583 }
584
588 public function exists() {
589 if ( !$this->mDataLoaded ) {
590 $this->loadPageData();
591 }
592 return $this->mId > 0;
593 }
594
603 public function hasViewableContent() {
604 return $this->mTitle->isKnown();
605 }
606
612 public function isRedirect() {
613 if ( !$this->mDataLoaded ) {
614 $this->loadPageData();
615 }
616
617 return (bool)$this->mIsRedirect;
618 }
619
630 public function getContentModel() {
631 if ( $this->exists() ) {
632 $cache = ObjectCache::getMainWANInstance();
633
634 return $cache->getWithSetCallback(
635 $cache->makeKey( 'page-content-model', $this->getLatest() ),
636 $cache::TTL_MONTH,
637 function () {
638 $rev = $this->getRevision();
639 if ( $rev ) {
640 // Look at the revision's actual content model
641 return $rev->getContentModel();
642 } else {
643 $title = $this->mTitle->getPrefixedDBkey();
644 wfWarn( "Page $title exists but has no (visible) revisions!" );
645 return $this->mTitle->getContentModel();
646 }
647 }
648 );
649 }
650
651 // use the default model for this page
652 return $this->mTitle->getContentModel();
653 }
654
659 public function checkTouched() {
660 if ( !$this->mDataLoaded ) {
661 $this->loadPageData();
662 }
663 return ( $this->mId && !$this->mIsRedirect );
664 }
665
670 public function getTouched() {
671 if ( !$this->mDataLoaded ) {
672 $this->loadPageData();
673 }
674 return $this->mTouched;
675 }
676
681 public function getLinksTimestamp() {
682 if ( !$this->mDataLoaded ) {
683 $this->loadPageData();
684 }
685 return $this->mLinksUpdated;
686 }
687
692 public function getLatest() {
693 if ( !$this->mDataLoaded ) {
694 $this->loadPageData();
695 }
696 return (int)$this->mLatest;
697 }
698
703 public function getOldestRevision() {
704 // Try using the replica DB first, then try the master
705 $rev = $this->mTitle->getFirstRevision();
706 if ( !$rev ) {
707 $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE );
708 }
709 return $rev;
710 }
711
716 protected function loadLastEdit() {
717 if ( $this->mLastRevision !== null ) {
718 return; // already loaded
719 }
720
721 $latest = $this->getLatest();
722 if ( !$latest ) {
723 return; // page doesn't exist or is missing page_latest info
724 }
725
726 if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
727 // T39225: if session S1 loads the page row FOR UPDATE, the result always
728 // includes the latest changes committed. This is true even within REPEATABLE-READ
729 // transactions, where S1 normally only sees changes committed before the first S1
730 // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
731 // may not find it since a page row UPDATE and revision row INSERT by S2 may have
732 // happened after the first S1 SELECT.
733 // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
734 $flags = Revision::READ_LOCKING;
735 $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
736 } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
737 // Bug T93976: if page_latest was loaded from the master, fetch the
738 // revision from there as well, as it may not exist yet on a replica DB.
739 // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
740 $flags = Revision::READ_LATEST;
741 $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
742 } else {
744 $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
745 }
746
747 if ( $revision ) { // sanity
748 $this->setLastEdit( $revision );
749 }
750 }
751
756 protected function setLastEdit( Revision $revision ) {
757 $this->mLastRevision = $revision;
758 $this->mTimestamp = $revision->getTimestamp();
759 }
760
765 public function getRevision() {
766 $this->loadLastEdit();
767 if ( $this->mLastRevision ) {
768 return $this->mLastRevision;
769 }
770 return null;
771 }
772
777 public function getRevisionRecord() {
778 $this->loadLastEdit();
779 if ( $this->mLastRevision ) {
780 return $this->mLastRevision->getRevisionRecord();
781 }
782 return null;
783 }
784
798 public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
799 $this->loadLastEdit();
800 if ( $this->mLastRevision ) {
801 return $this->mLastRevision->getContent( $audience, $user );
802 }
803 return null;
804 }
805
809 public function getTimestamp() {
810 // Check if the field has been filled by WikiPage::setTimestamp()
811 if ( !$this->mTimestamp ) {
812 $this->loadLastEdit();
813 }
814
815 return wfTimestamp( TS_MW, $this->mTimestamp );
816 }
817
823 public function setTimestamp( $ts ) {
824 $this->mTimestamp = wfTimestamp( TS_MW, $ts );
825 }
826
836 public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
837 $this->loadLastEdit();
838 if ( $this->mLastRevision ) {
839 return $this->mLastRevision->getUser( $audience, $user );
840 } else {
841 return -1;
842 }
843 }
844
855 public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
856 $revision = $this->getOldestRevision();
857 if ( $revision ) {
858 $userName = $revision->getUserText( $audience, $user );
859 return User::newFromName( $userName, false );
860 } else {
861 return null;
862 }
863 }
864
874 public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
875 $this->loadLastEdit();
876 if ( $this->mLastRevision ) {
877 return $this->mLastRevision->getUserText( $audience, $user );
878 } else {
879 return '';
880 }
881 }
882
892 public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
893 $this->loadLastEdit();
894 if ( $this->mLastRevision ) {
895 return $this->mLastRevision->getComment( $audience, $user );
896 } else {
897 return '';
898 }
899 }
900
906 public function getMinorEdit() {
907 $this->loadLastEdit();
908 if ( $this->mLastRevision ) {
909 return $this->mLastRevision->isMinor();
910 } else {
911 return false;
912 }
913 }
914
923 public function isCountable( $editInfo = false ) {
925
926 // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
927
928 if ( !$this->mTitle->isContentPage() ) {
929 return false;
930 }
931
932 if ( $editInfo ) {
933 // NOTE: only the main slot can make a page a redirect
934 $content = $editInfo->pstContent;
935 } else {
936 $content = $this->getContent();
937 }
938
939 if ( !$content || $content->isRedirect() ) {
940 return false;
941 }
942
943 $hasLinks = null;
944
945 if ( $wgArticleCountMethod === 'link' ) {
946 // nasty special case to avoid re-parsing to detect links
947
948 if ( $editInfo ) {
949 // ParserOutput::getLinks() is a 2D array of page links, so
950 // to be really correct we would need to recurse in the array
951 // but the main array should only have items in it if there are
952 // links.
953 $hasLinks = (bool)count( $editInfo->output->getLinks() );
954 } else {
955 // NOTE: keep in sync with revisionRenderer::getLinkCount
956 $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
957 [ 'pl_from' => $this->getId() ], __METHOD__ );
958 }
959 }
960
961 return $content->isCountable( $hasLinks );
962 }
963
971 public function getRedirectTarget() {
972 if ( !$this->mTitle->isRedirect() ) {
973 return null;
974 }
975
976 if ( $this->mRedirectTarget !== null ) {
977 return $this->mRedirectTarget;
978 }
979
980 // Query the redirect table
982 $row = $dbr->selectRow( 'redirect',
983 [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
984 [ 'rd_from' => $this->getId() ],
985 __METHOD__
986 );
987
988 // rd_fragment and rd_interwiki were added later, populate them if empty
989 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
990 // (T203942) We can't redirect to Media namespace because it's virtual.
991 // We don't want to modify Title objects farther down the
992 // line. So, let's fix this here by changing to File namespace.
993 if ( $row->rd_namespace == NS_MEDIA ) {
994 $namespace = NS_FILE;
995 } else {
996 $namespace = $row->rd_namespace;
997 }
998 $this->mRedirectTarget = Title::makeTitle(
999 $namespace, $row->rd_title,
1000 $row->rd_fragment, $row->rd_interwiki
1001 );
1002 return $this->mRedirectTarget;
1003 }
1004
1005 // This page doesn't have an entry in the redirect table
1006 $this->mRedirectTarget = $this->insertRedirect();
1007 return $this->mRedirectTarget;
1008 }
1009
1018 public function insertRedirect() {
1019 $content = $this->getContent();
1020 $retval = $content ? $content->getUltimateRedirectTarget() : null;
1021 if ( !$retval ) {
1022 return null;
1023 }
1024
1025 // Update the DB post-send if the page has not cached since now
1026 $latest = $this->getLatest();
1027 DeferredUpdates::addCallableUpdate(
1028 function () use ( $retval, $latest ) {
1029 $this->insertRedirectEntry( $retval, $latest );
1030 },
1031 DeferredUpdates::POSTSEND,
1033 );
1034
1035 return $retval;
1036 }
1037
1043 public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1044 $dbw = wfGetDB( DB_MASTER );
1045 $dbw->startAtomic( __METHOD__ );
1046
1047 if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1048 $dbw->upsert(
1049 'redirect',
1050 [
1051 'rd_from' => $this->getId(),
1052 'rd_namespace' => $rt->getNamespace(),
1053 'rd_title' => $rt->getDBkey(),
1054 'rd_fragment' => $rt->getFragment(),
1055 'rd_interwiki' => $rt->getInterwiki(),
1056 ],
1057 [ 'rd_from' ],
1058 [
1059 'rd_namespace' => $rt->getNamespace(),
1060 'rd_title' => $rt->getDBkey(),
1061 'rd_fragment' => $rt->getFragment(),
1062 'rd_interwiki' => $rt->getInterwiki(),
1063 ],
1064 __METHOD__
1065 );
1066 }
1067
1068 $dbw->endAtomic( __METHOD__ );
1069 }
1070
1076 public function followRedirect() {
1077 return $this->getRedirectURL( $this->getRedirectTarget() );
1078 }
1079
1087 public function getRedirectURL( $rt ) {
1088 if ( !$rt ) {
1089 return false;
1090 }
1091
1092 if ( $rt->isExternal() ) {
1093 if ( $rt->isLocal() ) {
1094 // Offsite wikis need an HTTP redirect.
1095 // This can be hard to reverse and may produce loops,
1096 // so they may be disabled in the site configuration.
1097 $source = $this->mTitle->getFullURL( 'redirect=no' );
1098 return $rt->getFullURL( [ 'rdfrom' => $source ] );
1099 } else {
1100 // External pages without "local" bit set are not valid
1101 // redirect targets
1102 return false;
1103 }
1104 }
1105
1106 if ( $rt->isSpecialPage() ) {
1107 // Gotta handle redirects to special pages differently:
1108 // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1109 // Some pages are not valid targets.
1110 if ( $rt->isValidRedirectTarget() ) {
1111 return $rt->getFullURL();
1112 } else {
1113 return false;
1114 }
1115 }
1116
1117 return $rt;
1118 }
1119
1125 public function getContributors() {
1126 // @todo: This is expensive; cache this info somewhere.
1127
1128 $dbr = wfGetDB( DB_REPLICA );
1129
1130 $actorMigration = ActorMigration::newMigration();
1131 $actorQuery = $actorMigration->getJoin( 'rev_user' );
1132
1133 $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1134
1135 $fields = [
1136 'user_id' => $actorQuery['fields']['rev_user'],
1137 'user_name' => $actorQuery['fields']['rev_user_text'],
1138 'actor_id' => $actorQuery['fields']['rev_actor'],
1139 'user_real_name' => 'MIN(user_real_name)',
1140 'timestamp' => 'MAX(rev_timestamp)',
1141 ];
1142
1143 $conds = [ 'rev_page' => $this->getId() ];
1144
1145 // The user who made the top revision gets credited as "this page was last edited by
1146 // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1147 $user = $this->getUser()
1148 ? User::newFromId( $this->getUser() )
1149 : User::newFromName( $this->getUserText(), false );
1150 $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1151
1152 // Username hidden?
1153 $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1154
1155 $jconds = [
1156 'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1157 ] + $actorQuery['joins'];
1158
1159 $options = [
1160 'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1161 'ORDER BY' => 'timestamp DESC',
1162 ];
1163
1164 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1165 return new UserArrayFromResult( $res );
1166 }
1167
1175 public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1176 return $parserOptions->getStubThreshold() == 0
1177 && $this->exists()
1178 && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1179 && $this->getContentHandler()->isParserCacheSupported();
1180 }
1181
1197 public function getParserOutput(
1198 ParserOptions $parserOptions, $oldid = null, $forceParse = false
1199 ) {
1200 $useParserCache =
1201 ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1202
1203 if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1204 throw new InvalidArgumentException(
1205 'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1206 );
1207 }
1208
1209 wfDebug( __METHOD__ .
1210 ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1211 if ( $parserOptions->getStubThreshold() ) {
1212 wfIncrStats( 'pcache.miss.stub' );
1213 }
1214
1215 if ( $useParserCache ) {
1216 $parserOutput = $this->getParserCache()
1217 ->get( $this, $parserOptions );
1218 if ( $parserOutput !== false ) {
1219 return $parserOutput;
1220 }
1221 }
1222
1223 if ( $oldid === null || $oldid === 0 ) {
1224 $oldid = $this->getLatest();
1225 }
1226
1227 $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1228 $pool->execute();
1229
1230 return $pool->getParserOutput();
1231 }
1232
1238 public function doViewUpdates( User $user, $oldid = 0 ) {
1239 if ( wfReadOnly() ) {
1240 return;
1241 }
1242
1243 // Update newtalk / watchlist notification status;
1244 // Avoid outage if the master is not reachable by using a deferred updated
1245 DeferredUpdates::addCallableUpdate(
1246 function () use ( $user, $oldid ) {
1247 Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1248
1249 $user->clearNotification( $this->mTitle, $oldid );
1250 },
1251 DeferredUpdates::PRESEND
1252 );
1253 }
1254
1261 public function doPurge() {
1262 // Avoid PHP 7.1 warning of passing $this by reference
1263 $wikiPage = $this;
1264
1265 if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1266 return false;
1267 }
1268
1269 $this->mTitle->invalidateCache();
1270
1271 // Clear file cache
1273 // Send purge after above page_touched update was committed
1274 DeferredUpdates::addUpdate(
1275 new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1276 DeferredUpdates::PRESEND
1277 );
1278
1279 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1280 $messageCache = MessageCache::singleton();
1281 $messageCache->updateMessageOverride( $this->mTitle, $this->getContent() );
1282 }
1283
1284 return true;
1285 }
1286
1303 public function insertOn( $dbw, $pageId = null ) {
1304 $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1305 $dbw->insert(
1306 'page',
1307 [
1308 'page_namespace' => $this->mTitle->getNamespace(),
1309 'page_title' => $this->mTitle->getDBkey(),
1310 'page_restrictions' => '',
1311 'page_is_redirect' => 0, // Will set this shortly...
1312 'page_is_new' => 1,
1313 'page_random' => wfRandom(),
1314 'page_touched' => $dbw->timestamp(),
1315 'page_latest' => 0, // Fill this in shortly...
1316 'page_len' => 0, // Fill this in shortly...
1317 ] + $pageIdForInsert,
1318 __METHOD__,
1319 'IGNORE'
1320 );
1321
1322 if ( $dbw->affectedRows() > 0 ) {
1323 $newid = $pageId ? (int)$pageId : $dbw->insertId();
1324 $this->mId = $newid;
1325 $this->mTitle->resetArticleID( $newid );
1326
1327 return $newid;
1328 } else {
1329 return false; // nothing changed
1330 }
1331 }
1332
1348 public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1349 $lastRevIsRedirect = null
1350 ) {
1352
1353 // TODO: move into PageUpdater or PageStore
1354 // NOTE: when doing that, make sure cached fields get reset in doEditContent,
1355 // and in the compat stub!
1356
1357 // Assertion to try to catch T92046
1358 if ( (int)$revision->getId() === 0 ) {
1359 throw new InvalidArgumentException(
1360 __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1361 );
1362 }
1363
1364 $content = $revision->getContent();
1365 $len = $content ? $content->getSize() : 0;
1366 $rt = $content ? $content->getUltimateRedirectTarget() : null;
1367
1368 $conditions = [ 'page_id' => $this->getId() ];
1369
1370 if ( !is_null( $lastRevision ) ) {
1371 // An extra check against threads stepping on each other
1372 $conditions['page_latest'] = $lastRevision;
1373 }
1374
1375 $revId = $revision->getId();
1376 Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1377
1378 $row = [ /* SET */
1379 'page_latest' => $revId,
1380 'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1381 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1382 'page_is_redirect' => $rt !== null ? 1 : 0,
1383 'page_len' => $len,
1384 ];
1385
1386 if ( $wgContentHandlerUseDB ) {
1387 $row['page_content_model'] = $revision->getContentModel();
1388 }
1389
1390 $dbw->update( 'page',
1391 $row,
1392 $conditions,
1393 __METHOD__ );
1394
1395 $result = $dbw->affectedRows() > 0;
1396 if ( $result ) {
1397 $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1398 $this->setLastEdit( $revision );
1399 $this->mLatest = $revision->getId();
1400 $this->mIsRedirect = (bool)$rt;
1401 // Update the LinkCache.
1402 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1403 $linkCache->addGoodLinkObj(
1404 $this->getId(),
1405 $this->mTitle,
1406 $len,
1407 $this->mIsRedirect,
1408 $this->mLatest,
1409 $revision->getContentModel()
1410 );
1411 }
1412
1413 return $result;
1414 }
1415
1427 public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1428 // Always update redirects (target link might have changed)
1429 // Update/Insert if we don't know if the last revision was a redirect or not
1430 // Delete if changing from redirect to non-redirect
1431 $isRedirect = !is_null( $redirectTitle );
1432
1433 if ( !$isRedirect && $lastRevIsRedirect === false ) {
1434 return true;
1435 }
1436
1437 if ( $isRedirect ) {
1438 $this->insertRedirectEntry( $redirectTitle );
1439 } else {
1440 // This is not a redirect, remove row from redirect table
1441 $where = [ 'rd_from' => $this->getId() ];
1442 $dbw->delete( 'redirect', $where, __METHOD__ );
1443 }
1444
1445 if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1446 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1447 }
1448
1449 return ( $dbw->affectedRows() != 0 );
1450 }
1451
1462 public function updateIfNewerOn( $dbw, $revision ) {
1463 $row = $dbw->selectRow(
1464 [ 'revision', 'page' ],
1465 [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1466 [
1467 'page_id' => $this->getId(),
1468 'page_latest=rev_id' ],
1469 __METHOD__ );
1470
1471 if ( $row ) {
1472 if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1473 return false;
1474 }
1475 $prev = $row->rev_id;
1476 $lastRevIsRedirect = (bool)$row->page_is_redirect;
1477 } else {
1478 // No or missing previous revision; mark the page as new
1479 $prev = 0;
1480 $lastRevIsRedirect = null;
1481 }
1482
1483 $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1484
1485 return $ret;
1486 }
1487
1500 public static function hasDifferencesOutsideMainSlot( Revision $a, Revision $b ) {
1501 $aSlots = $a->getRevisionRecord()->getSlots();
1502 $bSlots = $b->getRevisionRecord()->getSlots();
1503 $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1504
1505 return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1506 }
1507
1519 public function getUndoContent( Revision $undo, Revision $undoafter ) {
1520 // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1521
1522 if ( self::hasDifferencesOutsideMainSlot( $undo, $undoafter ) ) {
1523 // Cannot yet undo edits that involve anything other the main slot.
1524 return false;
1525 }
1526
1527 $handler = $undo->getContentHandler();
1528 return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1529 }
1530
1541 public function supportsSections() {
1542 return $this->getContentHandler()->supportsSections();
1543 }
1544
1559 public function replaceSectionContent(
1560 $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1561 ) {
1562 $baseRevId = null;
1563 if ( $edittime && $sectionId !== 'new' ) {
1564 $lb = $this->getDBLoadBalancer();
1565 $dbr = $lb->getConnection( DB_REPLICA );
1566 $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1567 // Try the master if this thread may have just added it.
1568 // This could be abstracted into a Revision method, but we don't want
1569 // to encourage loading of revisions by timestamp.
1570 if ( !$rev
1571 && $lb->getServerCount() > 1
1572 && $lb->hasOrMadeRecentMasterChanges()
1573 ) {
1574 $dbw = $lb->getConnection( DB_MASTER );
1575 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1576 }
1577 if ( $rev ) {
1578 $baseRevId = $rev->getId();
1579 }
1580 }
1581
1582 return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1583 }
1584
1598 public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1599 $sectionTitle = '', $baseRevId = null
1600 ) {
1601 if ( strval( $sectionId ) === '' ) {
1602 // Whole-page edit; let the whole text through
1603 $newContent = $sectionContent;
1604 } else {
1605 if ( !$this->supportsSections() ) {
1606 throw new MWException( "sections not supported for content model " .
1607 $this->getContentHandler()->getModelID() );
1608 }
1609
1610 // T32711: always use current version when adding a new section
1611 if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1612 $oldContent = $this->getContent();
1613 } else {
1614 $rev = Revision::newFromId( $baseRevId );
1615 if ( !$rev ) {
1616 wfDebug( __METHOD__ . " asked for bogus section (page: " .
1617 $this->getId() . "; section: $sectionId)\n" );
1618 return null;
1619 }
1620
1621 $oldContent = $rev->getContent();
1622 }
1623
1624 if ( !$oldContent ) {
1625 wfDebug( __METHOD__ . ": no page text\n" );
1626 return null;
1627 }
1628
1629 $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1630 }
1631
1632 return $newContent;
1633 }
1634
1644 public function checkFlags( $flags ) {
1645 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1646 if ( $this->exists() ) {
1647 $flags |= EDIT_UPDATE;
1648 } else {
1649 $flags |= EDIT_NEW;
1650 }
1651 }
1652
1653 return $flags;
1654 }
1655
1659 private function newDerivedDataUpdater() {
1661
1663 $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1664 $this->getRevisionStore(),
1665 $this->getRevisionRenderer(),
1666 $this->getParserCache(),
1667 JobQueueGroup::singleton(),
1668 MessageCache::singleton(),
1669 MediaWikiServices::getInstance()->getContentLanguage(),
1670 MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
1671 );
1672
1675
1676 return $derivedDataUpdater;
1677 }
1678
1706 private function getDerivedDataUpdater(
1707 User $forUser = null,
1708 RevisionRecord $forRevision = null,
1709 RevisionSlotsUpdate $forUpdate = null,
1710 $forEdit = false
1711 ) {
1712 if ( !$forRevision && !$forUpdate ) {
1713 // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1714 // going to use it with.
1715 $this->derivedDataUpdater = null;
1716 }
1717
1718 if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1719 // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1720 // to it did not yet initialize it, because we don't know what data it will be
1721 // initialized with.
1722 $this->derivedDataUpdater = null;
1723 }
1724
1725 // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1726 // However, there is no good way to construct a cache key. We'd need to check against all
1727 // cached instances.
1728
1729 if ( $this->derivedDataUpdater
1730 && !$this->derivedDataUpdater->isReusableFor(
1731 $forUser,
1732 $forRevision,
1733 $forUpdate,
1734 $forEdit ? $this->getLatest() : null
1735 )
1736 ) {
1737 $this->derivedDataUpdater = null;
1738 }
1739
1740 if ( !$this->derivedDataUpdater ) {
1741 $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1742 }
1743
1744 return $this->derivedDataUpdater;
1745 }
1746
1762 public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1764
1765 $pageUpdater = new PageUpdater(
1766 $user,
1767 $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1768 $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1769 $this->getDBLoadBalancer(),
1770 $this->getRevisionStore()
1771 );
1772
1773 $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
1774 $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
1775 $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
1776
1777 return $pageUpdater;
1778 }
1779
1842 public function doEditContent(
1843 Content $content, $summary, $flags = 0, $originalRevId = false,
1844 User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1845 ) {
1846 global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1847
1848 if ( !( $summary instanceof CommentStoreComment ) ) {
1849 $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1850 }
1851
1852 if ( !$user ) {
1853 $user = $wgUser;
1854 }
1855
1856 // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1857 // Checking the minoredit right should be done in the same place the 'bot' right is
1858 // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1859 if ( ( $flags & EDIT_MINOR ) && !$user->isAllowed( 'minoredit' ) ) {
1860 $flags = ( $flags & ~EDIT_MINOR );
1861 }
1862
1863 $slotsUpdate = new RevisionSlotsUpdate();
1864 $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1865
1866 // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1867 // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1868 // used by this PageUpdater. However, there is no guarantee for this.
1869 $updater = $this->newPageUpdater( $user, $slotsUpdate );
1870 $updater->setContent( SlotRecord::MAIN, $content );
1871 $updater->setOriginalRevisionId( $originalRevId );
1872 $updater->setUndidRevisionId( $undidRevId );
1873
1874 $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1875
1876 // TODO: this logic should not be in the storage layer, it's here for compatibility
1877 // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1878 // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1879 if ( $needsPatrol && $this->getTitle()->userCan( 'autopatrol', $user ) ) {
1880 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1881 }
1882
1883 $updater->addTags( $tags );
1884
1885 $revRec = $updater->saveRevision(
1886 $summary,
1887 $flags
1888 );
1889
1890 // $revRec will be null if the edit failed, or if no new revision was created because
1891 // the content did not change.
1892 if ( $revRec ) {
1893 // update cached fields
1894 // TODO: this is currently redundant to what is done in updateRevisionOn.
1895 // But updateRevisionOn() should move into PageStore, and then this will be needed.
1896 $this->setLastEdit( new Revision( $revRec ) ); // TODO: use RevisionRecord
1897 $this->mLatest = $revRec->getId();
1898 }
1899
1900 return $updater->getStatus();
1901 }
1902
1917 public function makeParserOptions( $context ) {
1918 $options = ParserOptions::newCanonical( $context );
1919
1920 if ( $this->getTitle()->isConversionTable() ) {
1921 // @todo ConversionTable should become a separate content model, so
1922 // we don't need special cases like this one.
1923 $options->disableContentConversion();
1924 }
1925
1926 return $options;
1927 }
1928
1949 public function prepareContentForEdit(
1951 $revision = null,
1952 User $user = null,
1953 $serialFormat = null,
1954 $useCache = true
1955 ) {
1956 global $wgUser;
1957
1958 if ( !$user ) {
1959 $user = $wgUser;
1960 }
1961
1962 if ( !is_object( $revision ) ) {
1963 $revid = $revision;
1964 // This code path is deprecated, and nothing is known to
1965 // use it, so performance here shouldn't be a worry.
1966 if ( $revid !== null ) {
1967 wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
1968 $store = $this->getRevisionStore();
1969 $revision = $store->getRevisionById( $revid, Revision::READ_LATEST );
1970 } else {
1971 $revision = null;
1972 }
1973 } elseif ( $revision instanceof Revision ) {
1974 $revision = $revision->getRevisionRecord();
1975 }
1976
1977 $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
1978 $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
1979
1980 if ( !$updater->isUpdatePrepared() ) {
1981 $updater->prepareContent( $user, $slots, $useCache );
1982
1983 if ( $revision ) {
1984 $updater->prepareUpdate(
1985 $revision,
1986 [
1987 'causeAction' => 'prepare-edit',
1988 'causeAgent' => $user->getName(),
1989 ]
1990 );
1991 }
1992 }
1993
1994 return $updater->getPreparedEdit();
1995 }
1996
2024 public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2025 $options += [
2026 'causeAction' => 'edit-page',
2027 'causeAgent' => $user->getName(),
2028 ];
2029
2030 $revision = $revision->getRevisionRecord();
2031
2032 $updater = $this->getDerivedDataUpdater( $user, $revision );
2033
2034 $updater->prepareUpdate( $revision, $options );
2035
2036 $updater->doUpdates();
2037 }
2038
2052 public function updateParserCache( array $options = [] ) {
2053 $revision = $this->getRevisionRecord();
2054 if ( !$revision || !$revision->getId() ) {
2055 LoggerFactory::getInstance( 'wikipage' )->info(
2056 __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2057 );
2058 return;
2059 }
2060 $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2061
2062 $updater = $this->getDerivedDataUpdater( $user, $revision );
2063 $updater->prepareUpdate( $revision, $options );
2064 $updater->doParserCacheUpdate();
2065 }
2066
2093 public function doSecondaryDataUpdates( array $options = [] ) {
2094 $options['recursive'] = $options['recursive'] ?? true;
2095 $revision = $this->getRevisionRecord();
2096 if ( !$revision || !$revision->getId() ) {
2097 LoggerFactory::getInstance( 'wikipage' )->info(
2098 __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2099 );
2100 return;
2101 }
2102 $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2103
2104 $updater = $this->getDerivedDataUpdater( $user, $revision );
2105 $updater->prepareUpdate( $revision, $options );
2106 $updater->doSecondaryDataUpdates( $options );
2107 }
2108
2123 public function doUpdateRestrictions( array $limit, array $expiry,
2124 &$cascade, $reason, User $user, $tags = null
2125 ) {
2127
2128 if ( wfReadOnly() ) {
2129 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2130 }
2131
2132 $this->loadPageData( 'fromdbmaster' );
2133 $restrictionTypes = $this->mTitle->getRestrictionTypes();
2134 $id = $this->getId();
2135
2136 if ( !$cascade ) {
2137 $cascade = false;
2138 }
2139
2140 // Take this opportunity to purge out expired restrictions
2141 Title::purgeExpiredRestrictions();
2142
2143 // @todo: Same limitations as described in ProtectionForm.php (line 37);
2144 // we expect a single selection, but the schema allows otherwise.
2145 $isProtected = false;
2146 $protect = false;
2147 $changed = false;
2148
2149 $dbw = wfGetDB( DB_MASTER );
2150
2151 foreach ( $restrictionTypes as $action ) {
2152 if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2153 $expiry[$action] = 'infinity';
2154 }
2155 if ( !isset( $limit[$action] ) ) {
2156 $limit[$action] = '';
2157 } elseif ( $limit[$action] != '' ) {
2158 $protect = true;
2159 }
2160
2161 // Get current restrictions on $action
2162 $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2163 if ( $current != '' ) {
2164 $isProtected = true;
2165 }
2166
2167 if ( $limit[$action] != $current ) {
2168 $changed = true;
2169 } elseif ( $limit[$action] != '' ) {
2170 // Only check expiry change if the action is actually being
2171 // protected, since expiry does nothing on an not-protected
2172 // action.
2173 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2174 $changed = true;
2175 }
2176 }
2177 }
2178
2179 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2180 $changed = true;
2181 }
2182
2183 // If nothing has changed, do nothing
2184 if ( !$changed ) {
2185 return Status::newGood();
2186 }
2187
2188 if ( !$protect ) { // No protection at all means unprotection
2189 $revCommentMsg = 'unprotectedarticle-comment';
2190 $logAction = 'unprotect';
2191 } elseif ( $isProtected ) {
2192 $revCommentMsg = 'modifiedarticleprotection-comment';
2193 $logAction = 'modify';
2194 } else {
2195 $revCommentMsg = 'protectedarticle-comment';
2196 $logAction = 'protect';
2197 }
2198
2199 $logRelationsValues = [];
2200 $logRelationsField = null;
2201 $logParamsDetails = [];
2202
2203 // Null revision (used for change tag insertion)
2204 $nullRevision = null;
2205
2206 if ( $id ) { // Protection of existing page
2207 // Avoid PHP 7.1 warning of passing $this by reference
2208 $wikiPage = $this;
2209
2210 if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2211 return Status::newGood();
2212 }
2213
2214 // Only certain restrictions can cascade...
2215 $editrestriction = isset( $limit['edit'] )
2216 ? [ $limit['edit'] ]
2217 : $this->mTitle->getRestrictions( 'edit' );
2218 foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2219 $editrestriction[$key] = 'editprotected'; // backwards compatibility
2220 }
2221 foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2222 $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2223 }
2224
2225 $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2226 foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2227 $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2228 }
2229 foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2230 $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2231 }
2232
2233 // The schema allows multiple restrictions
2234 if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2235 $cascade = false;
2236 }
2237
2238 // insert null revision to identify the page protection change as edit summary
2239 $latest = $this->getLatest();
2240 $nullRevision = $this->insertProtectNullRevision(
2241 $revCommentMsg,
2242 $limit,
2243 $expiry,
2244 $cascade,
2245 $reason,
2246 $user
2247 );
2248
2249 if ( $nullRevision === null ) {
2250 return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2251 }
2252
2253 $logRelationsField = 'pr_id';
2254
2255 // Update restrictions table
2256 foreach ( $limit as $action => $restrictions ) {
2257 $dbw->delete(
2258 'page_restrictions',
2259 [
2260 'pr_page' => $id,
2261 'pr_type' => $action
2262 ],
2263 __METHOD__
2264 );
2265 if ( $restrictions != '' ) {
2266 $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2267 $dbw->insert(
2268 'page_restrictions',
2269 [
2270 'pr_page' => $id,
2271 'pr_type' => $action,
2272 'pr_level' => $restrictions,
2273 'pr_cascade' => $cascadeValue,
2274 'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2275 ],
2276 __METHOD__
2277 );
2278 $logRelationsValues[] = $dbw->insertId();
2279 $logParamsDetails[] = [
2280 'type' => $action,
2281 'level' => $restrictions,
2282 'expiry' => $expiry[$action],
2283 'cascade' => (bool)$cascadeValue,
2284 ];
2285 }
2286 }
2287
2288 // Clear out legacy restriction fields
2289 $dbw->update(
2290 'page',
2291 [ 'page_restrictions' => '' ],
2292 [ 'page_id' => $id ],
2293 __METHOD__
2294 );
2295
2296 // Avoid PHP 7.1 warning of passing $this by reference
2297 $wikiPage = $this;
2298
2299 Hooks::run( 'NewRevisionFromEditComplete',
2300 [ $this, $nullRevision, $latest, $user ] );
2301 Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2302 } else { // Protection of non-existing page (also known as "title protection")
2303 // Cascade protection is meaningless in this case
2304 $cascade = false;
2305
2306 if ( $limit['create'] != '' ) {
2307 $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2308 $dbw->replace( 'protected_titles',
2309 [ [ 'pt_namespace', 'pt_title' ] ],
2310 [
2311 'pt_namespace' => $this->mTitle->getNamespace(),
2312 'pt_title' => $this->mTitle->getDBkey(),
2313 'pt_create_perm' => $limit['create'],
2314 'pt_timestamp' => $dbw->timestamp(),
2315 'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2316 'pt_user' => $user->getId(),
2317 ] + $commentFields, __METHOD__
2318 );
2319 $logParamsDetails[] = [
2320 'type' => 'create',
2321 'level' => $limit['create'],
2322 'expiry' => $expiry['create'],
2323 ];
2324 } else {
2325 $dbw->delete( 'protected_titles',
2326 [
2327 'pt_namespace' => $this->mTitle->getNamespace(),
2328 'pt_title' => $this->mTitle->getDBkey()
2329 ], __METHOD__
2330 );
2331 }
2332 }
2333
2334 $this->mTitle->flushRestrictions();
2335 InfoAction::invalidateCache( $this->mTitle );
2336
2337 if ( $logAction == 'unprotect' ) {
2338 $params = [];
2339 } else {
2340 $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2341 $params = [
2342 '4::description' => $protectDescriptionLog, // parameter for IRC
2343 '5:bool:cascade' => $cascade,
2344 'details' => $logParamsDetails, // parameter for localize and api
2345 ];
2346 }
2347
2348 // Update the protection log
2349 $logEntry = new ManualLogEntry( 'protect', $logAction );
2350 $logEntry->setTarget( $this->mTitle );
2351 $logEntry->setComment( $reason );
2352 $logEntry->setPerformer( $user );
2353 $logEntry->setParameters( $params );
2354 if ( !is_null( $nullRevision ) ) {
2355 $logEntry->setAssociatedRevId( $nullRevision->getId() );
2356 }
2357 $logEntry->setTags( $tags );
2358 if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2359 $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2360 }
2361 $logId = $logEntry->insert();
2362 $logEntry->publish( $logId );
2363
2364 return Status::newGood( $logId );
2365 }
2366
2378 public function insertProtectNullRevision( $revCommentMsg, array $limit,
2379 array $expiry, $cascade, $reason, $user = null
2380 ) {
2381 $dbw = wfGetDB( DB_MASTER );
2382
2383 // Prepare a null revision to be added to the history
2384 $editComment = wfMessage(
2385 $revCommentMsg,
2386 $this->mTitle->getPrefixedText(),
2387 $user ? $user->getName() : ''
2388 )->inContentLanguage()->text();
2389 if ( $reason ) {
2390 $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2391 }
2392 $protectDescription = $this->protectDescription( $limit, $expiry );
2393 if ( $protectDescription ) {
2394 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2395 $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2396 ->inContentLanguage()->text();
2397 }
2398 if ( $cascade ) {
2399 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2400 $editComment .= wfMessage( 'brackets' )->params(
2401 wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2402 )->inContentLanguage()->text();
2403 }
2404
2405 $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2406 if ( $nullRev ) {
2407 $nullRev->insertOn( $dbw );
2408
2409 // Update page record and touch page
2410 $oldLatest = $nullRev->getParentId();
2411 $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2412 }
2413
2414 return $nullRev;
2415 }
2416
2421 protected function formatExpiry( $expiry ) {
2422 if ( $expiry != 'infinity' ) {
2423 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2424 return wfMessage(
2425 'protect-expiring',
2426 $contLang->timeanddate( $expiry, false, false ),
2427 $contLang->date( $expiry, false, false ),
2428 $contLang->time( $expiry, false, false )
2429 )->inContentLanguage()->text();
2430 } else {
2431 return wfMessage( 'protect-expiry-indefinite' )
2432 ->inContentLanguage()->text();
2433 }
2434 }
2435
2443 public function protectDescription( array $limit, array $expiry ) {
2444 $protectDescription = '';
2445
2446 foreach ( array_filter( $limit ) as $action => $restrictions ) {
2447 # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2448 # All possible message keys are listed here for easier grepping:
2449 # * restriction-create
2450 # * restriction-edit
2451 # * restriction-move
2452 # * restriction-upload
2453 $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2454 # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2455 # with '' filtered out. All possible message keys are listed below:
2456 # * protect-level-autoconfirmed
2457 # * protect-level-sysop
2458 $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2459 ->inContentLanguage()->text();
2460
2461 $expiryText = $this->formatExpiry( $expiry[$action] );
2462
2463 if ( $protectDescription !== '' ) {
2464 $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2465 }
2466 $protectDescription .= wfMessage( 'protect-summary-desc' )
2467 ->params( $actionText, $restrictionsText, $expiryText )
2468 ->inContentLanguage()->text();
2469 }
2470
2471 return $protectDescription;
2472 }
2473
2485 public function protectDescriptionLog( array $limit, array $expiry ) {
2486 $protectDescriptionLog = '';
2487
2488 $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2489 foreach ( array_filter( $limit ) as $action => $restrictions ) {
2490 $expiryText = $this->formatExpiry( $expiry[$action] );
2491 $protectDescriptionLog .=
2492 $dirMark .
2493 "[$action=$restrictions] ($expiryText)";
2494 }
2495
2496 return trim( $protectDescriptionLog );
2497 }
2498
2508 protected static function flattenRestrictions( $limit ) {
2509 if ( !is_array( $limit ) ) {
2510 throw new MWException( __METHOD__ . ' given non-array restriction set' );
2511 }
2512
2513 $bits = [];
2514 ksort( $limit );
2515
2516 foreach ( array_filter( $limit ) as $action => $restrictions ) {
2517 $bits[] = "$action=$restrictions";
2518 }
2519
2520 return implode( ':', $bits );
2521 }
2522
2535 public function isBatchedDelete( $safetyMargin = 0 ) {
2537
2538 $dbr = wfGetDB( DB_REPLICA );
2539 $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2540 $revCount += $safetyMargin;
2541
2542 return $revCount >= $wgDeleteRevisionsBatchSize;
2543 }
2544
2564 public function doDeleteArticle(
2565 $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2566 $immediate = false
2567 ) {
2568 $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2569 [], 'delete', $immediate );
2570
2571 // Returns true if the page was actually deleted, or is scheduled for deletion
2572 return $status->isOK();
2573 }
2574
2597 public function doDeleteArticleReal(
2598 $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2599 $tags = [], $logsubtype = 'delete', $immediate = false
2600 ) {
2601 global $wgUser;
2602
2603 wfDebug( __METHOD__ . "\n" );
2604
2605 $status = Status::newGood();
2606
2607 // Avoid PHP 7.1 warning of passing $this by reference
2608 $wikiPage = $this;
2609
2610 $deleter = is_null( $deleter ) ? $wgUser : $deleter;
2611 if ( !Hooks::run( 'ArticleDelete',
2612 [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2613 ) ) {
2614 if ( $status->isOK() ) {
2615 // Hook aborted but didn't set a fatal status
2616 $status->fatal( 'delete-hook-aborted' );
2617 }
2618 return $status;
2619 }
2620
2621 return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2622 $logsubtype, $immediate );
2623 }
2624
2633 public function doDeleteArticleBatched(
2634 $reason, $suppress, User $deleter, $tags,
2635 $logsubtype, $immediate = false, $webRequestId = null
2636 ) {
2637 wfDebug( __METHOD__ . "\n" );
2638
2639 $status = Status::newGood();
2640
2641 $dbw = wfGetDB( DB_MASTER );
2642 $dbw->startAtomic( __METHOD__ );
2643
2644 $this->loadPageData( self::READ_LATEST );
2645 $id = $this->getId();
2646 // T98706: lock the page from various other updates but avoid using
2647 // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2648 // the revisions queries (which also JOIN on user). Only lock the page
2649 // row and CAS check on page_latest to see if the trx snapshot matches.
2650 $lockedLatest = $this->lockAndGetLatest();
2651 if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2652 $dbw->endAtomic( __METHOD__ );
2653 // Page not there or trx snapshot is stale
2654 $status->error( 'cannotdelete',
2655 wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2656 return $status;
2657 }
2658
2659 // At this point we are now committed to returning an OK
2660 // status unless some DB query error or other exception comes up.
2661 // This way callers don't have to call rollback() if $status is bad
2662 // unless they actually try to catch exceptions (which is rare).
2663
2664 // we need to remember the old content so we can use it to generate all deletion updates.
2665 $revision = $this->getRevision();
2666 try {
2667 $content = $this->getContent( Revision::RAW );
2668 } catch ( Exception $ex ) {
2669 wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2670 . $ex->getMessage() );
2671
2672 $content = null;
2673 }
2674
2675 // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2676 // one batch of revisions and defer archival of any others to the job queue.
2677 $explictTrxLogged = false;
2678 while ( true ) {
2679 $done = $this->archiveRevisions( $dbw, $id, $suppress );
2680 if ( $done || !$immediate ) {
2681 break;
2682 }
2683 $dbw->endAtomic( __METHOD__ );
2684 if ( $dbw->explicitTrxActive() ) {
2685 // Explict transactions may never happen here in practice. Log to be sure.
2686 if ( !$explictTrxLogged ) {
2687 $explictTrxLogged = true;
2688 LoggerFactory::getInstance( 'wfDebug' )->debug(
2689 'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2690 'title' => $this->getTitle()->getText(),
2691 ] );
2692 }
2693 continue;
2694 }
2695 if ( $dbw->trxLevel() ) {
2696 $dbw->commit();
2697 }
2698 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2699 $lbFactory->waitForReplication();
2700 $dbw->startAtomic( __METHOD__ );
2701 }
2702
2703 // If done archiving, also delete the article.
2704 if ( !$done ) {
2705 $dbw->endAtomic( __METHOD__ );
2706
2707 $jobParams = [
2708 'wikiPageId' => $id,
2709 'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2710 'reason' => $reason,
2711 'suppress' => $suppress,
2712 'userId' => $deleter->getId(),
2713 'tags' => json_encode( $tags ),
2714 'logsubtype' => $logsubtype,
2715 ];
2716
2717 $job = new DeletePageJob( $this->getTitle(), $jobParams );
2718 JobQueueGroup::singleton()->push( $job );
2719
2720 $status->warning( 'delete-scheduled',
2721 wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2722 } else {
2723 // Get archivedRevisionCount by db query, because there's no better alternative.
2724 // Jobs cannot pass a count of archived revisions to the next job, because additional
2725 // deletion operations can be started while the first is running. Jobs from each
2726 // gracefully interleave, but would not know about each other's count. Deduplication
2727 // in the job queue to avoid simultaneous deletion operations would add overhead.
2728 // Number of archived revisions cannot be known beforehand, because edits can be made
2729 // while deletion operations are being processed, changing the number of archivals.
2730 $archivedRevisionCount = $dbw->selectField(
2731 'archive', 'COUNT(*)',
2732 [
2733 'ar_namespace' => $this->getTitle()->getNamespace(),
2734 'ar_title' => $this->getTitle()->getDBkey(),
2735 'ar_page_id' => $id
2736 ], __METHOD__
2737 );
2738
2739 // Clone the title and wikiPage, so we have the information we need when
2740 // we log and run the ArticleDeleteComplete hook.
2741 $logTitle = clone $this->mTitle;
2742 $wikiPageBeforeDelete = clone $this;
2743
2744 // Now that it's safely backed up, delete it
2745 $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2746
2747 // Log the deletion, if the page was suppressed, put it in the suppression log instead
2748 $logtype = $suppress ? 'suppress' : 'delete';
2749
2750 $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2751 $logEntry->setPerformer( $deleter );
2752 $logEntry->setTarget( $logTitle );
2753 $logEntry->setComment( $reason );
2754 $logEntry->setTags( $tags );
2755 $logid = $logEntry->insert();
2756
2757 $dbw->onTransactionPreCommitOrIdle(
2758 function () use ( $logEntry, $logid ) {
2759 // T58776: avoid deadlocks (especially from FileDeleteForm)
2760 $logEntry->publish( $logid );
2761 },
2762 __METHOD__
2763 );
2764
2765 $dbw->endAtomic( __METHOD__ );
2766
2767 $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2768
2769 Hooks::run( 'ArticleDeleteComplete', [
2770 &$wikiPageBeforeDelete,
2771 &$deleter,
2772 $reason,
2773 $id,
2774 $content,
2775 $logEntry,
2776 $archivedRevisionCount
2777 ] );
2778 $status->value = $logid;
2779
2780 // Show log excerpt on 404 pages rather than just a link
2781 $cache = MediaWikiServices::getInstance()->getMainObjectStash();
2782 $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2783 $cache->set( $key, 1, $cache::TTL_DAY );
2784 }
2785
2786 return $status;
2787 }
2788
2798 protected function archiveRevisions( $dbw, $id, $suppress ) {
2802
2803 // Given the lock above, we can be confident in the title and page ID values
2804 $namespace = $this->getTitle()->getNamespace();
2805 $dbKey = $this->getTitle()->getDBkey();
2806
2807 $commentStore = CommentStore::getStore();
2808 $actorMigration = ActorMigration::newMigration();
2809
2811 $bitfield = false;
2812
2813 // Bitfields to further suppress the content
2814 if ( $suppress ) {
2815 $bitfield = Revision::SUPPRESSED_ALL;
2816 $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2817 }
2818
2819 // For now, shunt the revision data into the archive table.
2820 // Text is *not* removed from the text table; bulk storage
2821 // is left intact to avoid breaking block-compression or
2822 // immutable storage schemes.
2823 // In the future, we may keep revisions and mark them with
2824 // the rev_deleted field, which is reserved for this purpose.
2825
2826 // Lock rows in `revision` and its temp tables, but not any others.
2827 // Note array_intersect() preserves keys from the first arg, and we're
2828 // assuming $revQuery has `revision` primary and isn't using subtables
2829 // for anything we care about.
2830 $dbw->lockForUpdate(
2831 array_intersect(
2832 $revQuery['tables'],
2833 [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2834 ),
2835 [ 'rev_page' => $id ],
2836 __METHOD__,
2837 [],
2838 $revQuery['joins']
2839 );
2840
2841 // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
2842 // so we can copy it to the archive table.
2843 // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
2845 $revQuery['fields'][] = 'rev_text_id';
2846
2847 if ( $wgContentHandlerUseDB ) {
2848 $revQuery['fields'][] = 'rev_content_model';
2849 $revQuery['fields'][] = 'rev_content_format';
2850 }
2851 }
2852
2853 // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2854 // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2855 $res = $dbw->select(
2856 $revQuery['tables'],
2857 $revQuery['fields'],
2858 [ 'rev_page' => $id ],
2859 __METHOD__,
2860 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2861 $revQuery['joins']
2862 );
2863
2864 // Build their equivalent archive rows
2865 $rowsInsert = [];
2866 $revids = [];
2867
2869 $ipRevIds = [];
2870
2871 $done = true;
2872 foreach ( $res as $row ) {
2873 if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2874 $done = false;
2875 break;
2876 }
2877
2878 $comment = $commentStore->getComment( 'rev_comment', $row );
2879 $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2880 $rowInsert = [
2881 'ar_namespace' => $namespace,
2882 'ar_title' => $dbKey,
2883 'ar_timestamp' => $row->rev_timestamp,
2884 'ar_minor_edit' => $row->rev_minor_edit,
2885 'ar_rev_id' => $row->rev_id,
2886 'ar_parent_id' => $row->rev_parent_id,
2895 'ar_len' => $row->rev_len,
2896 'ar_page_id' => $id,
2897 'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2898 'ar_sha1' => $row->rev_sha1,
2899 ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2900 + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2901
2903 $rowInsert['ar_text_id'] = $row->rev_text_id;
2904
2905 if ( $wgContentHandlerUseDB ) {
2906 $rowInsert['ar_content_model'] = $row->rev_content_model;
2907 $rowInsert['ar_content_format'] = $row->rev_content_format;
2908 }
2909 }
2910
2911 $rowsInsert[] = $rowInsert;
2912 $revids[] = $row->rev_id;
2913
2914 // Keep track of IP edits, so that the corresponding rows can
2915 // be deleted in the ip_changes table.
2916 if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2917 $ipRevIds[] = $row->rev_id;
2918 }
2919 }
2920
2921 // This conditional is just a sanity check
2922 if ( count( $revids ) > 0 ) {
2923 // Copy them into the archive table
2924 $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2925
2926 $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2928 $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2929 }
2931 $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2932 }
2933
2934 // Also delete records from ip_changes as applicable.
2935 if ( count( $ipRevIds ) > 0 ) {
2936 $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2937 }
2938 }
2939
2940 return $done;
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
2977 public function doDeleteUpdates(
2978 $id, Content $content = null, Revision $revision = null, User $user = null
2979 ) {
2980 if ( $id !== $this->getId() ) {
2981 throw new InvalidArgumentException( 'Mismatching page ID' );
2982 }
2983
2984 try {
2985 $countable = $this->isCountable();
2986 } catch ( Exception $ex ) {
2987 // fallback for deleting broken pages for which we cannot load the content for
2988 // some reason. Note that doDeleteArticleReal() already logged this problem.
2989 $countable = false;
2990 }
2991
2992 // Update site status
2993 DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
2994 [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
2995 ) );
2996
2997 // Delete pagelinks, update secondary indexes, etc
2998 $updates = $this->getDeletionUpdates(
2999 $revision ? $revision->getRevisionRecord() : $content
3000 );
3001 foreach ( $updates as $update ) {
3002 DeferredUpdates::addUpdate( $update );
3003 }
3004
3005 $causeAgent = $user ? $user->getName() : 'unknown';
3006 // Reparse any pages transcluding this page
3007 LinksUpdate::queueRecursiveJobsForTable(
3008 $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3009 // Reparse any pages including this image
3010 if ( $this->mTitle->getNamespace() == NS_FILE ) {
3011 LinksUpdate::queueRecursiveJobsForTable(
3012 $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3013 }
3014
3015 // Clear caches
3016 self::onArticleDelete( $this->mTitle );
3017 ResourceLoaderWikiModule::invalidateModuleCache(
3018 $this->mTitle, $revision, null, wfWikiID()
3019 );
3020
3021 // Reset this object and the Title object
3022 $this->loadFromRow( false, self::READ_LATEST );
3023
3024 // Search engine
3025 DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3026 }
3027
3057 public function doRollback(
3058 $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3059 ) {
3060 $resultDetails = null;
3061
3062 // Check permissions
3063 $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3064 $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3065 $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3066
3067 if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3068 $errors[] = [ 'sessionfailure' ];
3069 }
3070
3071 if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3072 $errors[] = [ 'actionthrottledtext' ];
3073 }
3074
3075 // If there were errors, bail out now
3076 if ( !empty( $errors ) ) {
3077 return $errors;
3078 }
3079
3080 return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3081 }
3082
3103 public function commitRollback( $fromP, $summary, $bot,
3104 &$resultDetails, User $guser, $tags = null
3105 ) {
3106 global $wgUseRCPatrol;
3107
3108 $dbw = wfGetDB( DB_MASTER );
3109
3110 if ( wfReadOnly() ) {
3111 return [ [ 'readonlytext' ] ];
3112 }
3113
3114 // Begin revision creation cycle by creating a PageUpdater.
3115 // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3116 $updater = $this->newPageUpdater( $guser );
3117 $current = $updater->grabParentRevision();
3118
3119 if ( is_null( $current ) ) {
3120 // Something wrong... no page?
3121 return [ [ 'notanarticle' ] ];
3122 }
3123
3124 $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3125 $legacyCurrent = new Revision( $current );
3126 $from = str_replace( '_', ' ', $fromP );
3127
3128 // User name given should match up with the top revision.
3129 // If the revision's user is not visible, then $from should be empty.
3130 if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3131 $resultDetails = [ 'current' => $legacyCurrent ];
3132 return [ [ 'alreadyrolled',
3133 htmlspecialchars( $this->mTitle->getPrefixedText() ),
3134 htmlspecialchars( $fromP ),
3135 htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3136 ] ];
3137 }
3138
3139 // Get the last edit not by this person...
3140 // Note: these may not be public values
3141 $actorWhere = ActorMigration::newMigration()->getWhere(
3142 $dbw,
3143 'rev_user',
3144 $current->getUser( RevisionRecord::RAW )
3145 );
3146
3147 $s = $dbw->selectRow(
3148 [ 'revision' ] + $actorWhere['tables'],
3149 [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3150 [
3151 'rev_page' => $current->getPageId(),
3152 'NOT(' . $actorWhere['conds'] . ')',
3153 ],
3154 __METHOD__,
3155 [
3156 'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3157 'ORDER BY' => 'rev_timestamp DESC'
3158 ],
3159 $actorWhere['joins']
3160 );
3161 if ( $s === false ) {
3162 // No one else ever edited this page
3163 return [ [ 'cantrollback' ] ];
3164 } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3165 || $s->rev_deleted & RevisionRecord::DELETED_USER
3166 ) {
3167 // Only admins can see this text
3168 return [ [ 'notvisiblerev' ] ];
3169 }
3170
3171 // Generate the edit summary if necessary
3172 $target = $this->getRevisionStore()->getRevisionById(
3173 $s->rev_id,
3174 RevisionStore::READ_LATEST
3175 );
3176 if ( empty( $summary ) ) {
3177 if ( !$currentEditorForPublic ) { // no public user name
3178 $summary = wfMessage( 'revertpage-nouser' );
3179 } else {
3180 $summary = wfMessage( 'revertpage' );
3181 }
3182 }
3183 $legacyTarget = new Revision( $target );
3184 $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3185
3186 // Allow the custom summary to use the same args as the default message
3187 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3188 $args = [
3189 $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3190 $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3191 $s->rev_id,
3192 $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3193 $current->getId(),
3194 $contLang->timeanddate( $current->getTimestamp() )
3195 ];
3196 if ( $summary instanceof Message ) {
3197 $summary = $summary->params( $args )->inContentLanguage()->text();
3198 } else {
3199 $summary = wfMsgReplaceArgs( $summary, $args );
3200 }
3201
3202 // Trim spaces on user supplied text
3203 $summary = trim( $summary );
3204
3205 // Save
3206 $flags = EDIT_UPDATE | EDIT_INTERNAL;
3207
3208 if ( $guser->isAllowed( 'minoredit' ) ) {
3209 $flags |= EDIT_MINOR;
3210 }
3211
3212 if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3213 $flags |= EDIT_FORCE_BOT;
3214 }
3215
3216 // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3217 $currentContent = $current->getContent( SlotRecord::MAIN );
3218 $targetContent = $target->getContent( SlotRecord::MAIN );
3219 $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3220
3221 if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3222 $tags[] = 'mw-rollback';
3223 }
3224
3225 // Build rollback revision:
3226 // Restore old content
3227 // TODO: MCR: test this once we can store multiple slots
3228 foreach ( $target->getSlots()->getSlots() as $slot ) {
3229 $updater->inheritSlot( $slot );
3230 }
3231
3232 // Remove extra slots
3233 // TODO: MCR: test this once we can store multiple slots
3234 foreach ( $current->getSlotRoles() as $role ) {
3235 if ( !$target->hasSlot( $role ) ) {
3236 $updater->removeSlot( $role );
3237 }
3238 }
3239
3240 $updater->setOriginalRevisionId( $target->getId() );
3241 // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3242 $updater->addTags( $tags );
3243
3244 // TODO: this logic should not be in the storage layer, it's here for compatibility
3245 // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3246 // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3247 if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $guser ) ) {
3248 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3249 }
3250
3251 // Actually store the rollback
3252 $rev = $updater->saveRevision(
3253 CommentStoreComment::newUnsavedComment( $summary ),
3254 $flags
3255 );
3256
3257 // Set patrolling and bot flag on the edits, which gets rollbacked.
3258 // This is done even on edit failure to have patrolling in that case (T64157).
3259 $set = [];
3260 if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3261 // Mark all reverted edits as bot
3262 $set['rc_bot'] = 1;
3263 }
3264
3265 if ( $wgUseRCPatrol ) {
3266 // Mark all reverted edits as patrolled
3267 $set['rc_patrolled'] = RecentChange::PRC_PATROLLED;
3268 }
3269
3270 if ( count( $set ) ) {
3271 $actorWhere = ActorMigration::newMigration()->getWhere(
3272 $dbw,
3273 'rc_user',
3274 $current->getUser( RevisionRecord::RAW ),
3275 false
3276 );
3277 $dbw->update( 'recentchanges', $set,
3278 [ /* WHERE */
3279 'rc_cur_id' => $current->getPageId(),
3280 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3281 $actorWhere['conds'], // No tables/joins are needed for rc_user
3282 ],
3283 __METHOD__
3284 );
3285 }
3286
3287 if ( !$updater->wasSuccessful() ) {
3288 return $updater->getStatus()->getErrorsArray();
3289 }
3290
3291 // Report if the edit was not created because it did not change the content.
3292 if ( $updater->isUnchanged() ) {
3293 $resultDetails = [ 'current' => $legacyCurrent ];
3294 return [ [ 'alreadyrolled',
3295 htmlspecialchars( $this->mTitle->getPrefixedText() ),
3296 htmlspecialchars( $fromP ),
3297 htmlspecialchars( $targetEditorForPublic ? $targetEditorForPublic->getName() : '' )
3298 ] ];
3299 }
3300
3301 if ( $changingContentModel ) {
3302 // If the content model changed during the rollback,
3303 // make sure it gets logged to Special:Log/contentmodel
3304 $log = new ManualLogEntry( 'contentmodel', 'change' );
3305 $log->setPerformer( $guser );
3306 $log->setTarget( $this->mTitle );
3307 $log->setComment( $summary );
3308 $log->setParameters( [
3309 '4::oldmodel' => $currentContent->getModel(),
3310 '5::newmodel' => $targetContent->getModel(),
3311 ] );
3312
3313 $logId = $log->insert( $dbw );
3314 $log->publish( $logId );
3315 }
3316
3317 $revId = $rev->getId();
3318
3319 Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3320
3321 $resultDetails = [
3322 'summary' => $summary,
3323 'current' => $legacyCurrent,
3324 'target' => $legacyTarget,
3325 'newid' => $revId,
3326 'tags' => $tags
3327 ];
3328
3329 // TODO: make this return a Status object and wrap $resultDetails in that.
3330 return [];
3331 }
3332
3344 public static function onArticleCreate( Title $title ) {
3345 // TODO: move this into a PageEventEmitter service
3346
3347 // Update existence markers on article/talk tabs...
3348 $other = $title->getOtherPage();
3349
3350 $other->purgeSquid();
3351
3352 $title->touchLinks();
3353 $title->purgeSquid();
3354 $title->deleteTitleProtection();
3355
3356 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3357
3358 // Invalidate caches of articles which include this page
3359 DeferredUpdates::addUpdate(
3360 new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
3361 );
3362
3363 if ( $title->getNamespace() == NS_CATEGORY ) {
3364 // Load the Category object, which will schedule a job to create
3365 // the category table row if necessary. Checking a replica DB is ok
3366 // here, in the worst case it'll run an unnecessary recount job on
3367 // a category that probably doesn't have many members.
3368 Category::newFromTitle( $title )->getID();
3369 }
3370 }
3371
3377 public static function onArticleDelete( Title $title ) {
3378 // TODO: move this into a PageEventEmitter service
3379
3380 // Update existence markers on article/talk tabs...
3381 // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3382 BacklinkCache::get( $title )->clear();
3383 $other = $title->getOtherPage();
3384
3385 $other->purgeSquid();
3386
3387 $title->touchLinks();
3388 $title->purgeSquid();
3389
3390 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3391
3392 // File cache
3395
3396 // Messages
3397 if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3398 MessageCache::singleton()->updateMessageOverride( $title, null );
3399 }
3400
3401 // Images
3402 if ( $title->getNamespace() == NS_FILE ) {
3403 DeferredUpdates::addUpdate(
3404 new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
3405 );
3406 }
3407
3408 // User talk pages
3409 if ( $title->getNamespace() == NS_USER_TALK ) {
3410 $user = User::newFromName( $title->getText(), false );
3411 if ( $user ) {
3412 $user->setNewtalk( false );
3413 }
3414 }
3415
3416 // Image redirects
3417 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3418
3419 // Purge cross-wiki cache entities referencing this page
3420 self::purgeInterwikiCheckKey( $title );
3421 }
3422
3431 public static function onArticleEdit(
3432 Title $title,
3433 Revision $revision = null,
3434 $slotsChanged = null
3435 ) {
3436 // TODO: move this into a PageEventEmitter service
3437
3438 if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3439 // Invalidate caches of articles which include this page.
3440 // Only for the main slot, because only the main slot is transcluded.
3441 // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3442 DeferredUpdates::addUpdate(
3443 new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
3444 );
3445 }
3446
3447 // Invalidate the caches of all pages which redirect here
3448 DeferredUpdates::addUpdate(
3449 new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
3450 );
3451
3452 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3453
3454 // Purge CDN for this page only
3455 $title->purgeSquid();
3456 // Clear file cache for this page only
3458
3459 // Purge ?action=info cache
3460 $revid = $revision ? $revision->getId() : null;
3461 DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3463 } );
3464
3465 // Purge cross-wiki cache entities referencing this page
3466 self::purgeInterwikiCheckKey( $title );
3467 }
3468
3476 private static function purgeInterwikiCheckKey( Title $title ) {
3478
3480 return; // @todo: perhaps this wiki is only used as a *source* for content?
3481 }
3482
3483 DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3484 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3485 $cache->resetCheckKey(
3486 // Do not include the namespace since there can be multiple aliases to it
3487 // due to different namespace text definitions on different wikis. This only
3488 // means that some cache invalidations happen that are not strictly needed.
3489 $cache->makeGlobalKey( 'interwiki-page', wfWikiID(), $title->getDBkey() )
3490 );
3491 } );
3492 }
3493
3500 public function getCategories() {
3501 $id = $this->getId();
3502 if ( $id == 0 ) {
3503 return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3504 }
3505
3506 $dbr = wfGetDB( DB_REPLICA );
3507 $res = $dbr->select( 'categorylinks',
3508 [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3509 // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3510 // as not being aliases, and NS_CATEGORY is numeric
3511 [ 'cl_from' => $id ],
3512 __METHOD__ );
3513
3515 }
3516
3523 public function getHiddenCategories() {
3524 $result = [];
3525 $id = $this->getId();
3526
3527 if ( $id == 0 ) {
3528 return [];
3529 }
3530
3531 $dbr = wfGetDB( DB_REPLICA );
3532 $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3533 [ 'cl_to' ],
3534 [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3535 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3536 __METHOD__ );
3537
3538 if ( $res !== false ) {
3539 foreach ( $res as $row ) {
3540 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3541 }
3542 }
3543
3544 return $result;
3545 }
3546
3554 public function getAutoDeleteReason( &$hasHistory ) {
3555 return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3556 }
3557
3568 public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3569 $id = $id ?: $this->getId();
3570 $type = MWNamespace::getCategoryLinkType( $this->getTitle()->getNamespace() );
3571
3572 $addFields = [ 'cat_pages = cat_pages + 1' ];
3573 $removeFields = [ 'cat_pages = cat_pages - 1' ];
3574 if ( $type !== 'page' ) {
3575 $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3576 $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3577 }
3578
3579 $dbw = wfGetDB( DB_MASTER );
3580
3581 if ( count( $added ) ) {
3582 $existingAdded = $dbw->selectFieldValues(
3583 'category',
3584 'cat_title',
3585 [ 'cat_title' => $added ],
3586 __METHOD__
3587 );
3588
3589 // For category rows that already exist, do a plain
3590 // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3591 // to avoid creating gaps in the cat_id sequence.
3592 if ( count( $existingAdded ) ) {
3593 $dbw->update(
3594 'category',
3595 $addFields,
3596 [ 'cat_title' => $existingAdded ],
3597 __METHOD__
3598 );
3599 }
3600
3601 $missingAdded = array_diff( $added, $existingAdded );
3602 if ( count( $missingAdded ) ) {
3603 $insertRows = [];
3604 foreach ( $missingAdded as $cat ) {
3605 $insertRows[] = [
3606 'cat_title' => $cat,
3607 'cat_pages' => 1,
3608 'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3609 'cat_files' => ( $type === 'file' ) ? 1 : 0,
3610 ];
3611 }
3612 $dbw->upsert(
3613 'category',
3614 $insertRows,
3615 [ 'cat_title' ],
3616 $addFields,
3617 __METHOD__
3618 );
3619 }
3620 }
3621
3622 if ( count( $deleted ) ) {
3623 $dbw->update(
3624 'category',
3625 $removeFields,
3626 [ 'cat_title' => $deleted ],
3627 __METHOD__
3628 );
3629 }
3630
3631 foreach ( $added as $catName ) {
3632 $cat = Category::newFromName( $catName );
3633 Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3634 }
3635
3636 foreach ( $deleted as $catName ) {
3637 $cat = Category::newFromName( $catName );
3638 Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3639 // Refresh counts on categories that should be empty now (after commit, T166757)
3640 DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3641 $cat->refreshCountsIfEmpty();
3642 } );
3643 }
3644 }
3645
3652 public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3653 if ( wfReadOnly() ) {
3654 return;
3655 }
3656
3657 if ( !Hooks::run( 'OpportunisticLinksUpdate',
3658 [ $this, $this->mTitle, $parserOutput ]
3659 ) ) {
3660 return;
3661 }
3662
3663 $config = RequestContext::getMain()->getConfig();
3664
3665 $params = [
3666 'isOpportunistic' => true,
3667 'rootJobTimestamp' => $parserOutput->getCacheTime()
3668 ];
3669
3670 if ( $this->mTitle->areRestrictionsCascading() ) {
3671 // If the page is cascade protecting, the links should really be up-to-date
3672 JobQueueGroup::singleton()->lazyPush(
3674 );
3675 } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3676 // Assume the output contains "dynamic" time/random based magic words.
3677 // Only update pages that expired due to dynamic content and NOT due to edits
3678 // to referenced templates/files. When the cache expires due to dynamic content,
3679 // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3680 // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3681 // template/file edit already triggered recursive RefreshLinksJob jobs.
3682 if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3683 // If a page is uncacheable, do not keep spamming a job for it.
3684 // Although it would be de-duplicated, it would still waste I/O.
3685 $cache = ObjectCache::getLocalClusterInstance();
3686 $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3687 $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3688 if ( $cache->add( $key, time(), $ttl ) ) {
3689 JobQueueGroup::singleton()->lazyPush(
3690 RefreshLinksJob::newDynamic( $this->mTitle, $params )
3691 );
3692 }
3693 }
3694 }
3695 }
3696
3706 public function getDeletionUpdates( $rev = null ) {
3707 if ( !$rev ) {
3708 wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3709
3710 try {
3711 $rev = $this->getRevisionRecord();
3712 } catch ( Exception $ex ) {
3713 // If we can't load the content, something is wrong. Perhaps that's why
3714 // the user is trying to delete the page, so let's not fail in that case.
3715 // Note that doDeleteArticleReal() will already have logged an issue with
3716 // loading the content.
3717 wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3718 }
3719 }
3720
3721 if ( !$rev ) {
3722 $slotContent = [];
3723 } elseif ( $rev instanceof Content ) {
3724 wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3725
3726 $slotContent = [ SlotRecord::MAIN => $rev ];
3727 } else {
3728 $slotContent = array_map( function ( SlotRecord $slot ) {
3729 return $slot->getContent( Revision::RAW );
3730 }, $rev->getSlots()->getSlots() );
3731 }
3732
3733 $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3734
3735 // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3736 // model here, not the content object!
3737 // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3739 foreach ( $slotContent as $role => $content ) {
3740 $handler = $content->getContentHandler();
3741
3742 $updates = $handler->getDeletionUpdates(
3743 $this->getTitle(),
3744 $role
3745 );
3746 $allUpdates = array_merge( $allUpdates, $updates );
3747
3748 // TODO: remove B/C hack in 1.32!
3749 $legacyUpdates = $content->getDeletionUpdates( $this );
3750
3751 // HACK: filter out redundant and incomplete LinksDeletionUpdate
3752 $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3753 return !( $update instanceof LinksDeletionUpdate );
3754 } );
3755
3756 $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3757 }
3758
3759 Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3760
3761 // TODO: hard deprecate old hook in 1.33
3762 Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3763 return $allUpdates;
3764 }
3765
3773 public function isLocal() {
3774 return true;
3775 }
3776
3786 public function getWikiDisplayName() {
3787 global $wgSitename;
3788 return $wgSitename;
3789 }
3790
3799 public function getSourceURL() {
3800 return $this->getTitle()->getCanonicalURL();
3801 }
3802
3809 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3810
3811 return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3812 }
3813
3814}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
$wgDeleteRevisionsBatchSize
Page deletions with > this number of revisions will use the job queue.
$wgCascadingRestrictionLevels
Restriction levels that can be used with cascading protection.
int $wgMultiContentRevisionSchemaMigrationStage
RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables).
$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.
$wgPageCreationLog
Maintain a log of page creations at Special:Log/create?
$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.
$wgEnableScaryTranscluding
Enable interwiki transcluding.
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.
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage.
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.
wfIncrStats( $key, $count=1)
Increment a statistics counter.
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.
if( $line===false) $args
Definition cdb.php:64
static get(Title $title)
Create a new BacklinkCache or reuse any existing one.
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Handles purging appropriate CDN URLs given a title (or titles)
CommentStoreComment represents a comment stored by CommentStore.
Class DeletePageJob.
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.
Update object handling the cleanup of links tables after a page was deleted.
MediaWiki exception.
Class for creating new log entries and inserting them into the database.
Definition LogEntry.php:437
Represents information returned by WikiPage::prepareContentForEdit()
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
The RevisionRenderer service provides access to rendered output for revisions.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
A handle for managing updates for derived page data on edit, import, purge, etc.
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
Controller-like object for creating and updating pages by creating new revisions.
Value object representing a modification of revision slots.
The Message class provides methods which fulfil two basic services:
Definition Message.php:160
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 newPrioritized(Title $title, array $params)
static newDynamic(Title $title, array $params)
static singleton()
Get a RepoGroup instance.
Definition RepoGroup.php:59
getContentHandler()
Returns the content handler appropriate for this revision's content model.
Definition Revision.php:996
static newKnownCurrent(IDatabase $db, $pageIdOrTitle, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
static newFromPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
Definition Revision.php:152
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition Revision.php:291
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object.
Definition Revision.php:521
getRevisionRecord()
Definition Revision.php:637
const FOR_PUBLIC
Definition Revision.php:55
const SUPPRESSED_ALL
Definition Revision.php:52
const RAW
Definition Revision.php:57
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:114
Database independant search index updater.
static newFromResult( $res)
Represents a title within MediaWiki.
Definition Title.php:39
getNamespace()
Get the namespace index, i.e.
Definition Title.php:974
getFragment()
Get the Title fragment (i.e.
Definition Title.php:1587
getDBkey()
Get the main part with underscores.
Definition Title.php:951
getInterwiki()
Get the interwiki prefix.
Definition Title.php:861
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:47
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:592
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition User.php:3856
getId()
Get the user's ID.
Definition User.php:2437
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:682
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:615
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition User.php:658
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition User.php:3826
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:44
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition WikiPage.php:165
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...
doPurge()
Perform the actions of a page purging.
followRedirect()
Get the Title object or URL this page redirects to.
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
doDeleteArticleBatched( $reason, $suppress, User $deleter, $tags, $logsubtype, $immediate=false, $webRequestId=null)
Back-end article deletion.
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 purgeInterwikiCheckKey(Title $title)
#-
wasLoadedFrom( $from)
Checks whether the page data was loaded using the given database access mode (or better).
Definition WikiPage.php:512
prepareContentForEdit(Content $content, $revision=null, User $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
newDerivedDataUpdater()
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition WikiPage.php:127
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object's namespace and title using a sanitized title string.
Definition WikiPage.php:437
getTimestamp()
Definition WikiPage.php:809
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
isLocal()
Whether this content displayed on this page comes from the local database.
getRevision()
Get the latest revision.
Definition WikiPage.php:765
getUndoContent(Revision $undo, Revision $undoafter)
Get the content that needs to be saved in order to undo all revisions between $undo and $undoafter.
static onArticleEdit(Title $title, Revision $revision=null, $slotsChanged=null)
Purge caches on page update etc.
getLinksTimestamp()
Get the page_links_updated field.
Definition WikiPage.php:681
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition WikiPage.php:906
clearCacheFields()
Clear the object cache fields.
Definition WikiPage.php:295
getRevisionRenderer()
Definition WikiPage.php:231
Revision $mLastRevision
Definition WikiPage.php:81
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition WikiPage.php:315
getLatest()
Get the page_latest field.
Definition WikiPage.php:692
formatExpiry( $expiry)
getRevisionStore()
Definition WikiPage.php:224
PreparedEdit $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition WikiPage.php:61
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:66
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition WikiPage.php:115
loadFromRow( $data, $from)
Load the object from a database row.
Definition WikiPage.php:538
archiveRevisions( $dbw, $id, $suppress)
Archives revisions as part of page deletion.
supportsSections()
Returns true if this page's content model supports sections.
getRedirectTarget()
If this page is a redirect, get its target.
Definition WikiPage.php:971
DerivedPageDataUpdater null $derivedDataUpdater
Definition WikiPage.php:101
setTimestamp( $ts)
Set the page timestamp (use only to avoid DB queries)
Definition WikiPage.php:823
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:836
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:404
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:703
isBatchedDelete( $safetyMargin=0)
Determines if deletion of this page would be batched (executed over time by the job queue) or not (co...
replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle='', $baseRevId=null)
string $mTouched
Definition WikiPage.php:91
setLastEdit(Revision $revision)
Set the latest revision.
Definition WikiPage.php:756
getDBLoadBalancer()
Definition WikiPage.php:245
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:716
Title $mTitle
Definition WikiPage.php:50
getDerivedDataUpdater(User $forUser=null, RevisionRecord $forRevision=null, RevisionSlotsUpdate $forUpdate=null, $forEdit=false)
Returns a DerivedPageDataUpdater for use with the given target revision or new content.
getUserText( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition WikiPage.php:874
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition WikiPage.php:630
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition WikiPage.php:451
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.
getCategories()
Returns a list of categories this page is a member of.
doDeleteUpdates( $id, Content $content=null, Revision $revision=null, User $user=null)
Do some database updates after deletion.
string $mTimestamp
Timestamp of the current revision or empty string if not loaded.
Definition WikiPage.php:86
getHiddenCategories()
Returns a list of hidden categories this page is a member of.
doEditContent(Content $content, $summary, $flags=0, $originalRevId=false, User $user=null, $serialFormat=null, $tags=[], $undidRevId=0)
Change an existing article or create a new article.
getComment( $audience=Revision::FOR_PUBLIC, User $user=null)
Definition WikiPage.php:892
getDeletionUpdates( $rev=null)
Returns a list of updates to be performed when this page is deleted.
static newFromRow( $row, $from='fromdb')
Constructor from a database row.
Definition WikiPage.php:195
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
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.
updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect=null)
Add row to the redirect table if this is a redirect, remove otherwise.
hasViewableContent()
Check if this page is something we're going to be showing some sort of sensible content for.
Definition WikiPage.php:603
triggerOpportunisticLinksUpdate(ParserOutput $parserOutput)
Opportunistically enqueue link update jobs given fresh parser output if useful.
insertRedirect()
Insert an entry for this page into the redirect table if the content is a redirect.
static hasDifferencesOutsideMainSlot(Revision $a, Revision $b)
Helper method for checking whether two revisions have differences that go beyond the main slot.
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition WikiPage.php:798
getActionOverrides()
Definition WikiPage.php:255
updateParserCache(array $options=[])
Update the parser cache.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object.
Definition WikiPage.php:365
int $mDataLoadedFrom
One of the READ_* constants.
Definition WikiPage.php:71
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Title $mRedirectTarget
Definition WikiPage.php:76
newPageUpdater(User $user, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
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:326
insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason, $user=null)
Insert a new null revision for this page.
getTitle()
Get the title object of the article.
Definition WikiPage.php:276
isRedirect()
Tests if the article content represents a redirect.
Definition WikiPage.php:612
static onArticleCreate(Title $title)
The onArticle*() functions are supposed to be a kind of hooks which should be called whenever any of ...
doDeleteArticleReal( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $deleter=null, $tags=[], $logsubtype='delete', $immediate=false)
Back-end article deletion Deletes the article with database consistency, writes logs,...
string $mLinksUpdated
Definition WikiPage.php:96
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.
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition WikiPage.php:467
clear()
Clear the object.
Definition WikiPage.php:284
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition WikiPage.php:659
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:923
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:268
getRevisionRecord()
Get the latest revision.
Definition WikiPage.php:777
getWikiDisplayName()
The display name for the site this content come from.
getParserCache()
Definition WikiPage.php:238
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition WikiPage.php:207
getCreator( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition WikiPage.php:855
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:670
__construct(Title $title)
Constructor and clear the article.
Definition WikiPage.php:107
doDeleteArticle( $reason, $suppress=false, $u1=null, $u2=null, &$error='', User $user=null, $immediate=false)
Same as doDeleteArticleReal(), but returns a simple boolean.
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
Database connection, tracking, load balancing, and transaction manager for a cluster.
$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 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:156
const EDIT_INTERNAL
Definition Defines.php:159
const EDIT_UPDATE
Definition Defines.php:153
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:284
const NS_FILE
Definition Defines.php:70
const NS_MEDIAWIKI
Definition Defines.php:72
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:286
const NS_MEDIA
Definition Defines.php:52
const NS_USER_TALK
Definition Defines.php:67
const MIGRATION_OLD
Definition Defines.php:315
const EDIT_MINOR
Definition Defines.php:154
const NS_CATEGORY
Definition Defines.php:78
const EDIT_NEW
Definition Defines.php:152
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. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\d*-\d*)") will be honored when streaming the file. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED since 1.16! 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 since 1.28! 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:2042
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
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. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1305
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:2050
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:2885
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:994
this hook is for auditing only 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:1035
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:2054
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 use $formDescriptor instead 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
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:933
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:1818
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
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 function
Definition injection.txt:30
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
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:38
$cache
Definition mcc.php:33
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$source
$content
$page->newPageUpdater($user) $updater
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
if(count( $args)< 1) $job
$params