MediaWiki REL1_33
WikiPage.php
Go to the documentation of this file.
1<?php
34use Wikimedia\Assert\Assert;
38
45class WikiPage implements Page, IDBAccessObject {
46 // Constants for $mDataLoadedFrom and related
47
51 public $mTitle = null;
52
57 public $mDataLoaded = false;
58
63 public $mIsRedirect = false;
64
69 public $mLatest = false;
70
72 public $mPreparedEdit = false;
73
77 protected $mId = null;
78
82 protected $mDataLoadedFrom = self::READ_NONE;
83
87 protected $mRedirectTarget = null;
88
92 protected $mLastRevision = null;
93
97 protected $mTimestamp = '';
98
102 protected $mTouched = '19700101000000';
103
107 protected $mLinksUpdated = '19700101000000';
108
112 private $derivedDataUpdater = null;
113
118 public function __construct( Title $title ) {
119 $this->mTitle = $title;
120 }
121
126 public function __clone() {
127 $this->mTitle = clone $this->mTitle;
128 }
129
138 public static function factory( Title $title ) {
139 $ns = $title->getNamespace();
140
141 if ( $ns == NS_MEDIA ) {
142 throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
143 } elseif ( $ns < 0 ) {
144 throw new MWException( "Invalid or virtual namespace $ns given." );
145 }
146
147 $page = null;
148 if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
149 return $page;
150 }
151
152 switch ( $ns ) {
153 case NS_FILE:
154 $page = new WikiFilePage( $title );
155 break;
156 case NS_CATEGORY:
157 $page = new WikiCategoryPage( $title );
158 break;
159 default:
160 $page = new WikiPage( $title );
161 }
162
163 return $page;
164 }
165
176 public static function newFromID( $id, $from = 'fromdb' ) {
177 // page ids are never 0 or negative, see T63166
178 if ( $id < 1 ) {
179 return null;
180 }
181
182 $from = self::convertSelectType( $from );
183 $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
184 $pageQuery = self::getQueryInfo();
185 $row = $db->selectRow(
186 $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
187 [], $pageQuery['joins']
188 );
189 if ( !$row ) {
190 return null;
191 }
192 return self::newFromRow( $row, $from );
193 }
194
206 public static function newFromRow( $row, $from = 'fromdb' ) {
207 $page = self::factory( Title::newFromRow( $row ) );
208 $page->loadFromRow( $row, $from );
209 return $page;
210 }
211
218 protected static function convertSelectType( $type ) {
219 switch ( $type ) {
220 case 'fromdb':
221 return self::READ_NORMAL;
222 case 'fromdbmaster':
223 return self::READ_LATEST;
224 case 'forupdate':
225 return self::READ_LOCKING;
226 default:
227 // It may already be an integer or whatever else
228 return $type;
229 }
230 }
231
235 private function getRevisionStore() {
236 return MediaWikiServices::getInstance()->getRevisionStore();
237 }
238
242 private function getRevisionRenderer() {
243 return MediaWikiServices::getInstance()->getRevisionRenderer();
244 }
245
249 private function getSlotRoleRegistry() {
250 return MediaWikiServices::getInstance()->getSlotRoleRegistry();
251 }
252
256 private function getParserCache() {
257 return MediaWikiServices::getInstance()->getParserCache();
258 }
259
263 private function getDBLoadBalancer() {
264 return MediaWikiServices::getInstance()->getDBLoadBalancer();
265 }
266
273 public function getActionOverrides() {
274 return $this->getContentHandler()->getActionOverrides();
275 }
276
286 public function getContentHandler() {
287 return ContentHandler::getForModelID( $this->getContentModel() );
288 }
289
294 public function getTitle() {
295 return $this->mTitle;
296 }
297
302 public function clear() {
303 $this->mDataLoaded = false;
304 $this->mDataLoadedFrom = self::READ_NONE;
305
306 $this->clearCacheFields();
307 }
308
313 protected function clearCacheFields() {
314 $this->mId = null;
315 $this->mRedirectTarget = null; // Title object if set
316 $this->mLastRevision = null; // Latest revision
317 $this->mTouched = '19700101000000';
318 $this->mLinksUpdated = '19700101000000';
319 $this->mTimestamp = '';
320 $this->mIsRedirect = false;
321 $this->mLatest = false;
322 // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
323 // checks the requested rev ID and content against the cached one. For most
324 // content types, the output should not change during the lifetime of this cache.
325 // Clearing it can cause extra parses on edit for no reason.
326 }
327
333 public function clearPreparedEdit() {
334 $this->mPreparedEdit = false;
335 }
336
344 public static function selectFields() {
346
347 wfDeprecated( __METHOD__, '1.31' );
348
349 $fields = [
350 'page_id',
351 'page_namespace',
352 'page_title',
353 'page_restrictions',
354 'page_is_redirect',
355 'page_is_new',
356 'page_random',
357 'page_touched',
358 'page_links_updated',
359 'page_latest',
360 'page_len',
361 ];
362
364 $fields[] = 'page_content_model';
365 }
366
367 if ( $wgPageLanguageUseDB ) {
368 $fields[] = 'page_lang';
369 }
370
371 return $fields;
372 }
373
383 public static function getQueryInfo() {
385
386 $ret = [
387 'tables' => [ 'page' ],
388 'fields' => [
389 'page_id',
390 'page_namespace',
391 'page_title',
392 'page_restrictions',
393 'page_is_redirect',
394 'page_is_new',
395 'page_random',
396 'page_touched',
397 'page_links_updated',
398 'page_latest',
399 'page_len',
400 ],
401 'joins' => [],
402 ];
403
405 $ret['fields'][] = 'page_content_model';
406 }
407
408 if ( $wgPageLanguageUseDB ) {
409 $ret['fields'][] = 'page_lang';
410 }
411
412 return $ret;
413 }
414
422 protected function pageData( $dbr, $conditions, $options = [] ) {
423 $pageQuery = self::getQueryInfo();
424
425 // Avoid PHP 7.1 warning of passing $this by reference
426 $wikiPage = $this;
427
428 Hooks::run( 'ArticlePageDataBefore', [
429 &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
430 ] );
431
432 $row = $dbr->selectRow(
433 $pageQuery['tables'],
434 $pageQuery['fields'],
435 $conditions,
436 __METHOD__,
437 $options,
438 $pageQuery['joins']
439 );
440
441 Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
442
443 return $row;
444 }
445
455 public function pageDataFromTitle( $dbr, $title, $options = [] ) {
456 return $this->pageData( $dbr, [
457 'page_namespace' => $title->getNamespace(),
458 'page_title' => $title->getDBkey() ], $options );
459 }
460
469 public function pageDataFromId( $dbr, $id, $options = [] ) {
470 return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
471 }
472
485 public function loadPageData( $from = 'fromdb' ) {
486 $from = self::convertSelectType( $from );
487 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
488 // We already have the data from the correct location, no need to load it twice.
489 return;
490 }
491
492 if ( is_int( $from ) ) {
493 list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
494 $loadBalancer = $this->getDBLoadBalancer();
495 $db = $loadBalancer->getConnection( $index );
496 $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
497
498 if ( !$data
499 && $index == DB_REPLICA
500 && $loadBalancer->getServerCount() > 1
501 && $loadBalancer->hasOrMadeRecentMasterChanges()
502 ) {
503 $from = self::READ_LATEST;
504 list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
505 $db = $loadBalancer->getConnection( $index );
506 $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
507 }
508 } else {
509 // No idea from where the caller got this data, assume replica DB.
510 $data = $from;
511 $from = self::READ_NORMAL;
512 }
513
514 $this->loadFromRow( $data, $from );
515 }
516
530 public function wasLoadedFrom( $from ) {
531 $from = self::convertSelectType( $from );
532
533 if ( !is_int( $from ) ) {
534 // No idea from where the caller got this data, assume replica DB.
535 $from = self::READ_NORMAL;
536 }
537
538 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
539 return true;
540 }
541
542 return false;
543 }
544
556 public function loadFromRow( $data, $from ) {
557 $lc = MediaWikiServices::getInstance()->getLinkCache();
558 $lc->clearLink( $this->mTitle );
559
560 if ( $data ) {
561 $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
562
563 $this->mTitle->loadFromRow( $data );
564
565 // Old-fashioned restrictions
566 $this->mTitle->loadRestrictions( $data->page_restrictions );
567
568 $this->mId = intval( $data->page_id );
569 $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
570 $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
571 $this->mIsRedirect = intval( $data->page_is_redirect );
572 $this->mLatest = intval( $data->page_latest );
573 // T39225: $latest may no longer match the cached latest Revision object.
574 // Double-check the ID of any cached latest Revision object for consistency.
575 if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
576 $this->mLastRevision = null;
577 $this->mTimestamp = '';
578 }
579 } else {
580 $lc->addBadLinkObj( $this->mTitle );
581
582 $this->mTitle->loadFromRow( false );
583
584 $this->clearCacheFields();
585
586 $this->mId = 0;
587 }
588
589 $this->mDataLoaded = true;
590 $this->mDataLoadedFrom = self::convertSelectType( $from );
591 }
592
596 public function getId() {
597 if ( !$this->mDataLoaded ) {
598 $this->loadPageData();
599 }
600 return $this->mId;
601 }
602
606 public function exists() {
607 if ( !$this->mDataLoaded ) {
608 $this->loadPageData();
609 }
610 return $this->mId > 0;
611 }
612
621 public function hasViewableContent() {
622 return $this->mTitle->isKnown();
623 }
624
630 public function isRedirect() {
631 if ( !$this->mDataLoaded ) {
632 $this->loadPageData();
633 }
634
635 return (bool)$this->mIsRedirect;
636 }
637
648 public function getContentModel() {
649 if ( $this->exists() ) {
650 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
651
652 return $cache->getWithSetCallback(
653 $cache->makeKey( 'page-content-model', $this->getLatest() ),
654 $cache::TTL_MONTH,
655 function () {
656 $rev = $this->getRevision();
657 if ( $rev ) {
658 // Look at the revision's actual content model
659 return $rev->getContentModel();
660 } else {
661 $title = $this->mTitle->getPrefixedDBkey();
662 wfWarn( "Page $title exists but has no (visible) revisions!" );
663 return $this->mTitle->getContentModel();
664 }
665 }
666 );
667 }
668
669 // use the default model for this page
670 return $this->mTitle->getContentModel();
671 }
672
677 public function checkTouched() {
678 if ( !$this->mDataLoaded ) {
679 $this->loadPageData();
680 }
681 return ( $this->mId && !$this->mIsRedirect );
682 }
683
688 public function getTouched() {
689 if ( !$this->mDataLoaded ) {
690 $this->loadPageData();
691 }
692 return $this->mTouched;
693 }
694
699 public function getLinksTimestamp() {
700 if ( !$this->mDataLoaded ) {
701 $this->loadPageData();
702 }
703 return $this->mLinksUpdated;
704 }
705
710 public function getLatest() {
711 if ( !$this->mDataLoaded ) {
712 $this->loadPageData();
713 }
714 return (int)$this->mLatest;
715 }
716
721 public function getOldestRevision() {
722 // Try using the replica DB first, then try the master
723 $rev = $this->mTitle->getFirstRevision();
724 if ( !$rev ) {
725 $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE );
726 }
727 return $rev;
728 }
729
734 protected function loadLastEdit() {
735 if ( $this->mLastRevision !== null ) {
736 return; // already loaded
737 }
738
739 $latest = $this->getLatest();
740 if ( !$latest ) {
741 return; // page doesn't exist or is missing page_latest info
742 }
743
744 if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
745 // T39225: if session S1 loads the page row FOR UPDATE, the result always
746 // includes the latest changes committed. This is true even within REPEATABLE-READ
747 // transactions, where S1 normally only sees changes committed before the first S1
748 // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
749 // may not find it since a page row UPDATE and revision row INSERT by S2 may have
750 // happened after the first S1 SELECT.
751 // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
752 $flags = Revision::READ_LOCKING;
753 $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
754 } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
755 // Bug T93976: if page_latest was loaded from the master, fetch the
756 // revision from there as well, as it may not exist yet on a replica DB.
757 // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
758 $flags = Revision::READ_LATEST;
759 $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
760 } else {
762 $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
763 }
764
765 if ( $revision ) { // sanity
766 $this->setLastEdit( $revision );
767 }
768 }
769
774 protected function setLastEdit( Revision $revision ) {
775 $this->mLastRevision = $revision;
776 $this->mTimestamp = $revision->getTimestamp();
777 }
778
783 public function getRevision() {
784 $this->loadLastEdit();
785 if ( $this->mLastRevision ) {
786 return $this->mLastRevision;
787 }
788 return null;
789 }
790
795 public function getRevisionRecord() {
796 $this->loadLastEdit();
797 if ( $this->mLastRevision ) {
798 return $this->mLastRevision->getRevisionRecord();
799 }
800 return null;
801 }
802
816 public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
817 $this->loadLastEdit();
818 if ( $this->mLastRevision ) {
819 return $this->mLastRevision->getContent( $audience, $user );
820 }
821 return null;
822 }
823
827 public function getTimestamp() {
828 // Check if the field has been filled by WikiPage::setTimestamp()
829 if ( !$this->mTimestamp ) {
830 $this->loadLastEdit();
831 }
832
833 return wfTimestamp( TS_MW, $this->mTimestamp );
834 }
835
841 public function setTimestamp( $ts ) {
842 $this->mTimestamp = wfTimestamp( TS_MW, $ts );
843 }
844
854 public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
855 $this->loadLastEdit();
856 if ( $this->mLastRevision ) {
857 return $this->mLastRevision->getUser( $audience, $user );
858 } else {
859 return -1;
860 }
861 }
862
873 public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
874 $revision = $this->getOldestRevision();
875 if ( $revision ) {
876 $userName = $revision->getUserText( $audience, $user );
877 return User::newFromName( $userName, false );
878 } else {
879 return null;
880 }
881 }
882
892 public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
893 $this->loadLastEdit();
894 if ( $this->mLastRevision ) {
895 return $this->mLastRevision->getUserText( $audience, $user );
896 } else {
897 return '';
898 }
899 }
900
911 public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
912 $this->loadLastEdit();
913 if ( $this->mLastRevision ) {
914 return $this->mLastRevision->getComment( $audience, $user );
915 } else {
916 return '';
917 }
918 }
919
925 public function getMinorEdit() {
926 $this->loadLastEdit();
927 if ( $this->mLastRevision ) {
928 return $this->mLastRevision->isMinor();
929 } else {
930 return false;
931 }
932 }
933
942 public function isCountable( $editInfo = false ) {
944
945 // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
946
947 if ( !$this->mTitle->isContentPage() ) {
948 return false;
949 }
950
951 if ( $editInfo ) {
952 // NOTE: only the main slot can make a page a redirect
953 $content = $editInfo->pstContent;
954 } else {
955 $content = $this->getContent();
956 }
957
958 if ( !$content || $content->isRedirect() ) {
959 return false;
960 }
961
962 $hasLinks = null;
963
964 if ( $wgArticleCountMethod === 'link' ) {
965 // nasty special case to avoid re-parsing to detect links
966
967 if ( $editInfo ) {
968 // ParserOutput::getLinks() is a 2D array of page links, so
969 // to be really correct we would need to recurse in the array
970 // but the main array should only have items in it if there are
971 // links.
972 $hasLinks = (bool)count( $editInfo->output->getLinks() );
973 } else {
974 // NOTE: keep in sync with RevisionRenderer::getLinkCount
975 // NOTE: keep in sync with DerivedPageDataUpdater::isCountable
976 $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
977 [ 'pl_from' => $this->getId() ], __METHOD__ );
978 }
979 }
980
981 // TODO: MCR: determine $hasLinks for each slot, and use that info
982 // with that slot's Content's isCountable method. That requires per-
983 // slot ParserOutput in the ParserCache, or per-slot info in the
984 // pagelinks table.
985 return $content->isCountable( $hasLinks );
986 }
987
995 public function getRedirectTarget() {
996 if ( !$this->mTitle->isRedirect() ) {
997 return null;
998 }
999
1000 if ( $this->mRedirectTarget !== null ) {
1001 return $this->mRedirectTarget;
1002 }
1003
1004 // Query the redirect table
1005 $dbr = wfGetDB( DB_REPLICA );
1006 $row = $dbr->selectRow( 'redirect',
1007 [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1008 [ 'rd_from' => $this->getId() ],
1009 __METHOD__
1010 );
1011
1012 // rd_fragment and rd_interwiki were added later, populate them if empty
1013 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
1014 // (T203942) We can't redirect to Media namespace because it's virtual.
1015 // We don't want to modify Title objects farther down the
1016 // line. So, let's fix this here by changing to File namespace.
1017 if ( $row->rd_namespace == NS_MEDIA ) {
1018 $namespace = NS_FILE;
1019 } else {
1020 $namespace = $row->rd_namespace;
1021 }
1022 $this->mRedirectTarget = Title::makeTitle(
1023 $namespace, $row->rd_title,
1024 $row->rd_fragment, $row->rd_interwiki
1025 );
1026 return $this->mRedirectTarget;
1027 }
1028
1029 // This page doesn't have an entry in the redirect table
1030 $this->mRedirectTarget = $this->insertRedirect();
1031 return $this->mRedirectTarget;
1032 }
1033
1042 public function insertRedirect() {
1043 $content = $this->getContent();
1044 $retval = $content ? $content->getUltimateRedirectTarget() : null;
1045 if ( !$retval ) {
1046 return null;
1047 }
1048
1049 // Update the DB post-send if the page has not cached since now
1050 $latest = $this->getLatest();
1051 DeferredUpdates::addCallableUpdate(
1052 function () use ( $retval, $latest ) {
1053 $this->insertRedirectEntry( $retval, $latest );
1054 },
1055 DeferredUpdates::POSTSEND,
1057 );
1058
1059 return $retval;
1060 }
1061
1067 public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
1068 $dbw = wfGetDB( DB_MASTER );
1069 $dbw->startAtomic( __METHOD__ );
1070
1071 if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
1072 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1073 $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
1074 $dbw->upsert(
1075 'redirect',
1076 [
1077 'rd_from' => $this->getId(),
1078 'rd_namespace' => $rt->getNamespace(),
1079 'rd_title' => $rt->getDBkey(),
1080 'rd_fragment' => $truncatedFragment,
1081 'rd_interwiki' => $rt->getInterwiki(),
1082 ],
1083 [ 'rd_from' ],
1084 [
1085 'rd_namespace' => $rt->getNamespace(),
1086 'rd_title' => $rt->getDBkey(),
1087 'rd_fragment' => $truncatedFragment,
1088 'rd_interwiki' => $rt->getInterwiki(),
1089 ],
1090 __METHOD__
1091 );
1092 }
1093
1094 $dbw->endAtomic( __METHOD__ );
1095 }
1096
1102 public function followRedirect() {
1103 return $this->getRedirectURL( $this->getRedirectTarget() );
1104 }
1105
1113 public function getRedirectURL( $rt ) {
1114 if ( !$rt ) {
1115 return false;
1116 }
1117
1118 if ( $rt->isExternal() ) {
1119 if ( $rt->isLocal() ) {
1120 // Offsite wikis need an HTTP redirect.
1121 // This can be hard to reverse and may produce loops,
1122 // so they may be disabled in the site configuration.
1123 $source = $this->mTitle->getFullURL( 'redirect=no' );
1124 return $rt->getFullURL( [ 'rdfrom' => $source ] );
1125 } else {
1126 // External pages without "local" bit set are not valid
1127 // redirect targets
1128 return false;
1129 }
1130 }
1131
1132 if ( $rt->isSpecialPage() ) {
1133 // Gotta handle redirects to special pages differently:
1134 // Fill the HTTP response "Location" header and ignore the rest of the page we're on.
1135 // Some pages are not valid targets.
1136 if ( $rt->isValidRedirectTarget() ) {
1137 return $rt->getFullURL();
1138 } else {
1139 return false;
1140 }
1141 }
1142
1143 return $rt;
1144 }
1145
1151 public function getContributors() {
1152 // @todo: This is expensive; cache this info somewhere.
1153
1154 $dbr = wfGetDB( DB_REPLICA );
1155
1156 $actorMigration = ActorMigration::newMigration();
1157 $actorQuery = $actorMigration->getJoin( 'rev_user' );
1158
1159 $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
1160
1161 $fields = [
1162 'user_id' => $actorQuery['fields']['rev_user'],
1163 'user_name' => $actorQuery['fields']['rev_user_text'],
1164 'actor_id' => $actorQuery['fields']['rev_actor'],
1165 'user_real_name' => 'MIN(user_real_name)',
1166 'timestamp' => 'MAX(rev_timestamp)',
1167 ];
1168
1169 $conds = [ 'rev_page' => $this->getId() ];
1170
1171 // The user who made the top revision gets credited as "this page was last edited by
1172 // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1173 $user = $this->getUser()
1174 ? User::newFromId( $this->getUser() )
1175 : User::newFromName( $this->getUserText(), false );
1176 $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
1177
1178 // Username hidden?
1179 $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1180
1181 $jconds = [
1182 'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
1183 ] + $actorQuery['joins'];
1184
1185 $options = [
1186 'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
1187 'ORDER BY' => 'timestamp DESC',
1188 ];
1189
1190 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1191 return new UserArrayFromResult( $res );
1192 }
1193
1201 public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1202 return $parserOptions->getStubThreshold() == 0
1203 && $this->exists()
1204 && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1205 && $this->getContentHandler()->isParserCacheSupported();
1206 }
1207
1223 public function getParserOutput(
1224 ParserOptions $parserOptions, $oldid = null, $forceParse = false
1225 ) {
1226 $useParserCache =
1227 ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1228
1229 if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
1230 throw new InvalidArgumentException(
1231 'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1232 );
1233 }
1234
1235 wfDebug( __METHOD__ .
1236 ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1237 if ( $parserOptions->getStubThreshold() ) {
1238 wfIncrStats( 'pcache.miss.stub' );
1239 }
1240
1241 if ( $useParserCache ) {
1242 $parserOutput = $this->getParserCache()
1243 ->get( $this, $parserOptions );
1244 if ( $parserOutput !== false ) {
1245 return $parserOutput;
1246 }
1247 }
1248
1249 if ( $oldid === null || $oldid === 0 ) {
1250 $oldid = $this->getLatest();
1251 }
1252
1253 $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1254 $pool->execute();
1255
1256 return $pool->getParserOutput();
1257 }
1258
1264 public function doViewUpdates( User $user, $oldid = 0 ) {
1265 if ( wfReadOnly() ) {
1266 return;
1267 }
1268
1269 // Update newtalk / watchlist notification status;
1270 // Avoid outage if the master is not reachable by using a deferred updated
1271 DeferredUpdates::addCallableUpdate(
1272 function () use ( $user, $oldid ) {
1273 Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1274
1275 $user->clearNotification( $this->mTitle, $oldid );
1276 },
1277 DeferredUpdates::PRESEND
1278 );
1279 }
1280
1287 public function doPurge() {
1288 // Avoid PHP 7.1 warning of passing $this by reference
1289 $wikiPage = $this;
1290
1291 if ( !Hooks::run( 'ArticlePurge', [ &$wikiPage ] ) ) {
1292 return false;
1293 }
1294
1295 $this->mTitle->invalidateCache();
1296
1297 // Clear file cache
1299 // Send purge after above page_touched update was committed
1300 DeferredUpdates::addUpdate(
1301 new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1302 DeferredUpdates::PRESEND
1303 );
1304
1305 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1306 $messageCache = MessageCache::singleton();
1307 $messageCache->updateMessageOverride( $this->mTitle, $this->getContent() );
1308 }
1309
1310 return true;
1311 }
1312
1329 public function insertOn( $dbw, $pageId = null ) {
1330 $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
1331 $dbw->insert(
1332 'page',
1333 [
1334 'page_namespace' => $this->mTitle->getNamespace(),
1335 'page_title' => $this->mTitle->getDBkey(),
1336 'page_restrictions' => '',
1337 'page_is_redirect' => 0, // Will set this shortly...
1338 'page_is_new' => 1,
1339 'page_random' => wfRandom(),
1340 'page_touched' => $dbw->timestamp(),
1341 'page_latest' => 0, // Fill this in shortly...
1342 'page_len' => 0, // Fill this in shortly...
1343 ] + $pageIdForInsert,
1344 __METHOD__,
1345 'IGNORE'
1346 );
1347
1348 if ( $dbw->affectedRows() > 0 ) {
1349 $newid = $pageId ? (int)$pageId : $dbw->insertId();
1350 $this->mId = $newid;
1351 $this->mTitle->resetArticleID( $newid );
1352
1353 return $newid;
1354 } else {
1355 return false; // nothing changed
1356 }
1357 }
1358
1374 public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1375 $lastRevIsRedirect = null
1376 ) {
1378
1379 // TODO: move into PageUpdater or PageStore
1380 // NOTE: when doing that, make sure cached fields get reset in doEditContent,
1381 // and in the compat stub!
1382
1383 // Assertion to try to catch T92046
1384 if ( (int)$revision->getId() === 0 ) {
1385 throw new InvalidArgumentException(
1386 __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1387 );
1388 }
1389
1390 $content = $revision->getContent();
1391 $len = $content ? $content->getSize() : 0;
1392 $rt = $content ? $content->getUltimateRedirectTarget() : null;
1393
1394 $conditions = [ 'page_id' => $this->getId() ];
1395
1396 if ( !is_null( $lastRevision ) ) {
1397 // An extra check against threads stepping on each other
1398 $conditions['page_latest'] = $lastRevision;
1399 }
1400
1401 $revId = $revision->getId();
1402 Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
1403
1404 $row = [ /* SET */
1405 'page_latest' => $revId,
1406 'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1407 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1408 'page_is_redirect' => $rt !== null ? 1 : 0,
1409 'page_len' => $len,
1410 ];
1411
1412 if ( $wgContentHandlerUseDB ) {
1413 $row['page_content_model'] = $revision->getContentModel();
1414 }
1415
1416 $dbw->update( 'page',
1417 $row,
1418 $conditions,
1419 __METHOD__ );
1420
1421 $result = $dbw->affectedRows() > 0;
1422 if ( $result ) {
1423 $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1424 $this->setLastEdit( $revision );
1425 $this->mLatest = $revision->getId();
1426 $this->mIsRedirect = (bool)$rt;
1427 // Update the LinkCache.
1428 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1429 $linkCache->addGoodLinkObj(
1430 $this->getId(),
1431 $this->mTitle,
1432 $len,
1433 $this->mIsRedirect,
1434 $this->mLatest,
1435 $revision->getContentModel()
1436 );
1437 }
1438
1439 return $result;
1440 }
1441
1453 public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1454 // Always update redirects (target link might have changed)
1455 // Update/Insert if we don't know if the last revision was a redirect or not
1456 // Delete if changing from redirect to non-redirect
1457 $isRedirect = !is_null( $redirectTitle );
1458
1459 if ( !$isRedirect && $lastRevIsRedirect === false ) {
1460 return true;
1461 }
1462
1463 if ( $isRedirect ) {
1464 $this->insertRedirectEntry( $redirectTitle );
1465 } else {
1466 // This is not a redirect, remove row from redirect table
1467 $where = [ 'rd_from' => $this->getId() ];
1468 $dbw->delete( 'redirect', $where, __METHOD__ );
1469 }
1470
1471 if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1472 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1473 }
1474
1475 return ( $dbw->affectedRows() != 0 );
1476 }
1477
1488 public function updateIfNewerOn( $dbw, $revision ) {
1489 $row = $dbw->selectRow(
1490 [ 'revision', 'page' ],
1491 [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1492 [
1493 'page_id' => $this->getId(),
1494 'page_latest=rev_id' ],
1495 __METHOD__ );
1496
1497 if ( $row ) {
1498 if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1499 return false;
1500 }
1501 $prev = $row->rev_id;
1502 $lastRevIsRedirect = (bool)$row->page_is_redirect;
1503 } else {
1504 // No or missing previous revision; mark the page as new
1505 $prev = 0;
1506 $lastRevIsRedirect = null;
1507 }
1508
1509 $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1510
1511 return $ret;
1512 }
1513
1526 public static function hasDifferencesOutsideMainSlot( Revision $a, Revision $b ) {
1527 $aSlots = $a->getRevisionRecord()->getSlots();
1528 $bSlots = $b->getRevisionRecord()->getSlots();
1529 $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
1530
1531 return ( $changedRoles !== [ SlotRecord::MAIN ] && $changedRoles !== [] );
1532 }
1533
1545 public function getUndoContent( Revision $undo, Revision $undoafter ) {
1546 // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
1547
1548 if ( self::hasDifferencesOutsideMainSlot( $undo, $undoafter ) ) {
1549 // Cannot yet undo edits that involve anything other the main slot.
1550 return false;
1551 }
1552
1553 $handler = $undo->getContentHandler();
1554 return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1555 }
1556
1567 public function supportsSections() {
1568 return $this->getContentHandler()->supportsSections();
1569 }
1570
1585 public function replaceSectionContent(
1586 $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1587 ) {
1588 $baseRevId = null;
1589 if ( $edittime && $sectionId !== 'new' ) {
1590 $lb = $this->getDBLoadBalancer();
1591 $dbr = $lb->getConnection( DB_REPLICA );
1592 $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1593 // Try the master if this thread may have just added it.
1594 // This could be abstracted into a Revision method, but we don't want
1595 // to encourage loading of revisions by timestamp.
1596 if ( !$rev
1597 && $lb->getServerCount() > 1
1598 && $lb->hasOrMadeRecentMasterChanges()
1599 ) {
1600 $dbw = $lb->getConnection( DB_MASTER );
1601 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1602 }
1603 if ( $rev ) {
1604 $baseRevId = $rev->getId();
1605 }
1606 }
1607
1608 return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1609 }
1610
1624 public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1625 $sectionTitle = '', $baseRevId = null
1626 ) {
1627 if ( strval( $sectionId ) === '' ) {
1628 // Whole-page edit; let the whole text through
1629 $newContent = $sectionContent;
1630 } else {
1631 if ( !$this->supportsSections() ) {
1632 throw new MWException( "sections not supported for content model " .
1633 $this->getContentHandler()->getModelID() );
1634 }
1635
1636 // T32711: always use current version when adding a new section
1637 if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1638 $oldContent = $this->getContent();
1639 } else {
1640 $rev = Revision::newFromId( $baseRevId );
1641 if ( !$rev ) {
1642 wfDebug( __METHOD__ . " asked for bogus section (page: " .
1643 $this->getId() . "; section: $sectionId)\n" );
1644 return null;
1645 }
1646
1647 $oldContent = $rev->getContent();
1648 }
1649
1650 if ( !$oldContent ) {
1651 wfDebug( __METHOD__ . ": no page text\n" );
1652 return null;
1653 }
1654
1655 $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1656 }
1657
1658 return $newContent;
1659 }
1660
1670 public function checkFlags( $flags ) {
1671 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1672 if ( $this->exists() ) {
1673 $flags |= EDIT_UPDATE;
1674 } else {
1675 $flags |= EDIT_NEW;
1676 }
1677 }
1678
1679 return $flags;
1680 }
1681
1685 private function newDerivedDataUpdater() {
1687
1689 $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1690 $this->getRevisionStore(),
1691 $this->getRevisionRenderer(),
1692 $this->getSlotRoleRegistry(),
1693 $this->getParserCache(),
1694 JobQueueGroup::singleton(),
1695 MessageCache::singleton(),
1696 MediaWikiServices::getInstance()->getContentLanguage(),
1697 MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
1698 );
1699
1702
1703 return $derivedDataUpdater;
1704 }
1705
1733 private function getDerivedDataUpdater(
1734 User $forUser = null,
1735 RevisionRecord $forRevision = null,
1736 RevisionSlotsUpdate $forUpdate = null,
1737 $forEdit = false
1738 ) {
1739 if ( !$forRevision && !$forUpdate ) {
1740 // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
1741 // going to use it with.
1742 $this->derivedDataUpdater = null;
1743 }
1744
1745 if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
1746 // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
1747 // to it did not yet initialize it, because we don't know what data it will be
1748 // initialized with.
1749 $this->derivedDataUpdater = null;
1750 }
1751
1752 // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
1753 // However, there is no good way to construct a cache key. We'd need to check against all
1754 // cached instances.
1755
1756 if ( $this->derivedDataUpdater
1757 && !$this->derivedDataUpdater->isReusableFor(
1758 $forUser,
1759 $forRevision,
1760 $forUpdate,
1761 $forEdit ? $this->getLatest() : null
1762 )
1763 ) {
1764 $this->derivedDataUpdater = null;
1765 }
1766
1767 if ( !$this->derivedDataUpdater ) {
1768 $this->derivedDataUpdater = $this->newDerivedDataUpdater();
1769 }
1770
1771 return $this->derivedDataUpdater;
1772 }
1773
1789 public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
1791
1792 $pageUpdater = new PageUpdater(
1793 $user,
1794 $this, // NOTE: eventually, PageUpdater should not know about WikiPage
1795 $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
1796 $this->getDBLoadBalancer(),
1797 $this->getRevisionStore(),
1798 $this->getSlotRoleRegistry()
1799 );
1800
1801 $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
1802 $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
1803 $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
1804
1805 return $pageUpdater;
1806 }
1807
1870 public function doEditContent(
1871 Content $content, $summary, $flags = 0, $originalRevId = false,
1872 User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
1873 ) {
1874 global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
1875
1876 if ( !( $summary instanceof CommentStoreComment ) ) {
1877 $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
1878 }
1879
1880 if ( !$user ) {
1881 $user = $wgUser;
1882 }
1883
1884 // TODO: this check is here for backwards-compatibility with 1.31 behavior.
1885 // Checking the minoredit right should be done in the same place the 'bot' right is
1886 // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
1887 if ( ( $flags & EDIT_MINOR ) && !$user->isAllowed( 'minoredit' ) ) {
1888 $flags = ( $flags & ~EDIT_MINOR );
1889 }
1890
1891 $slotsUpdate = new RevisionSlotsUpdate();
1892 $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
1893
1894 // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
1895 // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
1896 // used by this PageUpdater. However, there is no guarantee for this.
1897 $updater = $this->newPageUpdater( $user, $slotsUpdate );
1898 $updater->setContent( SlotRecord::MAIN, $content );
1899 $updater->setOriginalRevisionId( $originalRevId );
1900 $updater->setUndidRevisionId( $undidRevId );
1901
1902 $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
1903
1904 // TODO: this logic should not be in the storage layer, it's here for compatibility
1905 // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
1906 // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
1907 if ( $needsPatrol && $this->getTitle()->userCan( 'autopatrol', $user ) ) {
1908 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
1909 }
1910
1911 $updater->addTags( $tags );
1912
1913 $revRec = $updater->saveRevision(
1914 $summary,
1915 $flags
1916 );
1917
1918 // $revRec will be null if the edit failed, or if no new revision was created because
1919 // the content did not change.
1920 if ( $revRec ) {
1921 // update cached fields
1922 // TODO: this is currently redundant to what is done in updateRevisionOn.
1923 // But updateRevisionOn() should move into PageStore, and then this will be needed.
1924 $this->setLastEdit( new Revision( $revRec ) ); // TODO: use RevisionRecord
1925 $this->mLatest = $revRec->getId();
1926 }
1927
1928 return $updater->getStatus();
1929 }
1930
1945 public function makeParserOptions( $context ) {
1946 $options = ParserOptions::newCanonical( $context );
1947
1948 if ( $this->getTitle()->isConversionTable() ) {
1949 // @todo ConversionTable should become a separate content model, so
1950 // we don't need special cases like this one.
1951 $options->disableContentConversion();
1952 }
1953
1954 return $options;
1955 }
1956
1977 public function prepareContentForEdit(
1979 $revision = null,
1980 User $user = null,
1981 $serialFormat = null,
1982 $useCache = true
1983 ) {
1984 global $wgUser;
1985
1986 if ( !$user ) {
1987 $user = $wgUser;
1988 }
1989
1990 if ( !is_object( $revision ) ) {
1991 $revid = $revision;
1992 // This code path is deprecated, and nothing is known to
1993 // use it, so performance here shouldn't be a worry.
1994 if ( $revid !== null ) {
1995 wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
1996 $store = $this->getRevisionStore();
1997 $revision = $store->getRevisionById( $revid, Revision::READ_LATEST );
1998 } else {
1999 $revision = null;
2000 }
2001 } elseif ( $revision instanceof Revision ) {
2002 $revision = $revision->getRevisionRecord();
2003 }
2004
2005 $slots = RevisionSlotsUpdate::newFromContent( [ SlotRecord::MAIN => $content ] );
2006 $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
2007
2008 if ( !$updater->isUpdatePrepared() ) {
2009 $updater->prepareContent( $user, $slots, $useCache );
2010
2011 if ( $revision ) {
2012 $updater->prepareUpdate(
2013 $revision,
2014 [
2015 'causeAction' => 'prepare-edit',
2016 'causeAgent' => $user->getName(),
2017 ]
2018 );
2019 }
2020 }
2021
2022 return $updater->getPreparedEdit();
2023 }
2024
2052 public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2053 $options += [
2054 'causeAction' => 'edit-page',
2055 'causeAgent' => $user->getName(),
2056 ];
2057
2058 $revision = $revision->getRevisionRecord();
2059
2060 $updater = $this->getDerivedDataUpdater( $user, $revision );
2061
2062 $updater->prepareUpdate( $revision, $options );
2063
2064 $updater->doUpdates();
2065 }
2066
2080 public function updateParserCache( array $options = [] ) {
2081 $revision = $this->getRevisionRecord();
2082 if ( !$revision || !$revision->getId() ) {
2083 LoggerFactory::getInstance( 'wikipage' )->info(
2084 __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2085 );
2086 return;
2087 }
2088 $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2089
2090 $updater = $this->getDerivedDataUpdater( $user, $revision );
2091 $updater->prepareUpdate( $revision, $options );
2092 $updater->doParserCacheUpdate();
2093 }
2094
2121 public function doSecondaryDataUpdates( array $options = [] ) {
2122 $options['recursive'] = $options['recursive'] ?? true;
2123 $revision = $this->getRevisionRecord();
2124 if ( !$revision || !$revision->getId() ) {
2125 LoggerFactory::getInstance( 'wikipage' )->info(
2126 __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision'
2127 );
2128 return;
2129 }
2130 $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) );
2131
2132 $updater = $this->getDerivedDataUpdater( $user, $revision );
2133 $updater->prepareUpdate( $revision, $options );
2134 $updater->doSecondaryDataUpdates( $options );
2135 }
2136
2151 public function doUpdateRestrictions( array $limit, array $expiry,
2152 &$cascade, $reason, User $user, $tags = null
2153 ) {
2155
2156 if ( wfReadOnly() ) {
2157 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
2158 }
2159
2160 $this->loadPageData( 'fromdbmaster' );
2161 $this->mTitle->loadRestrictions( null, Title::READ_LATEST );
2162 $restrictionTypes = $this->mTitle->getRestrictionTypes();
2163 $id = $this->getId();
2164
2165 if ( !$cascade ) {
2166 $cascade = false;
2167 }
2168
2169 // Take this opportunity to purge out expired restrictions
2170 Title::purgeExpiredRestrictions();
2171
2172 // @todo: Same limitations as described in ProtectionForm.php (line 37);
2173 // we expect a single selection, but the schema allows otherwise.
2174 $isProtected = false;
2175 $protect = false;
2176 $changed = false;
2177
2178 $dbw = wfGetDB( DB_MASTER );
2179
2180 foreach ( $restrictionTypes as $action ) {
2181 if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2182 $expiry[$action] = 'infinity';
2183 }
2184 if ( !isset( $limit[$action] ) ) {
2185 $limit[$action] = '';
2186 } elseif ( $limit[$action] != '' ) {
2187 $protect = true;
2188 }
2189
2190 // Get current restrictions on $action
2191 $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2192 if ( $current != '' ) {
2193 $isProtected = true;
2194 }
2195
2196 if ( $limit[$action] != $current ) {
2197 $changed = true;
2198 } elseif ( $limit[$action] != '' ) {
2199 // Only check expiry change if the action is actually being
2200 // protected, since expiry does nothing on an not-protected
2201 // action.
2202 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2203 $changed = true;
2204 }
2205 }
2206 }
2207
2208 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2209 $changed = true;
2210 }
2211
2212 // If nothing has changed, do nothing
2213 if ( !$changed ) {
2214 return Status::newGood();
2215 }
2216
2217 if ( !$protect ) { // No protection at all means unprotection
2218 $revCommentMsg = 'unprotectedarticle-comment';
2219 $logAction = 'unprotect';
2220 } elseif ( $isProtected ) {
2221 $revCommentMsg = 'modifiedarticleprotection-comment';
2222 $logAction = 'modify';
2223 } else {
2224 $revCommentMsg = 'protectedarticle-comment';
2225 $logAction = 'protect';
2226 }
2227
2228 $logRelationsValues = [];
2229 $logRelationsField = null;
2230 $logParamsDetails = [];
2231
2232 // Null revision (used for change tag insertion)
2233 $nullRevision = null;
2234
2235 if ( $id ) { // Protection of existing page
2236 // Avoid PHP 7.1 warning of passing $this by reference
2237 $wikiPage = $this;
2238
2239 if ( !Hooks::run( 'ArticleProtect', [ &$wikiPage, &$user, $limit, $reason ] ) ) {
2240 return Status::newGood();
2241 }
2242
2243 // Only certain restrictions can cascade...
2244 $editrestriction = isset( $limit['edit'] )
2245 ? [ $limit['edit'] ]
2246 : $this->mTitle->getRestrictions( 'edit' );
2247 foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2248 $editrestriction[$key] = 'editprotected'; // backwards compatibility
2249 }
2250 foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2251 $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2252 }
2253
2254 $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2255 foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2256 $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2257 }
2258 foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2259 $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2260 }
2261
2262 // The schema allows multiple restrictions
2263 if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2264 $cascade = false;
2265 }
2266
2267 // insert null revision to identify the page protection change as edit summary
2268 $latest = $this->getLatest();
2269 $nullRevision = $this->insertProtectNullRevision(
2270 $revCommentMsg,
2271 $limit,
2272 $expiry,
2273 $cascade,
2274 $reason,
2275 $user
2276 );
2277
2278 if ( $nullRevision === null ) {
2279 return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2280 }
2281
2282 $logRelationsField = 'pr_id';
2283
2284 // Update restrictions table
2285 foreach ( $limit as $action => $restrictions ) {
2286 $dbw->delete(
2287 'page_restrictions',
2288 [
2289 'pr_page' => $id,
2290 'pr_type' => $action
2291 ],
2292 __METHOD__
2293 );
2294 if ( $restrictions != '' ) {
2295 $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2296 $dbw->insert(
2297 'page_restrictions',
2298 [
2299 'pr_page' => $id,
2300 'pr_type' => $action,
2301 'pr_level' => $restrictions,
2302 'pr_cascade' => $cascadeValue,
2303 'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2304 ],
2305 __METHOD__
2306 );
2307 $logRelationsValues[] = $dbw->insertId();
2308 $logParamsDetails[] = [
2309 'type' => $action,
2310 'level' => $restrictions,
2311 'expiry' => $expiry[$action],
2312 'cascade' => (bool)$cascadeValue,
2313 ];
2314 }
2315 }
2316
2317 // Clear out legacy restriction fields
2318 $dbw->update(
2319 'page',
2320 [ 'page_restrictions' => '' ],
2321 [ 'page_id' => $id ],
2322 __METHOD__
2323 );
2324
2325 // Avoid PHP 7.1 warning of passing $this by reference
2326 $wikiPage = $this;
2327
2328 Hooks::run( 'NewRevisionFromEditComplete',
2329 [ $this, $nullRevision, $latest, $user ] );
2330 Hooks::run( 'ArticleProtectComplete', [ &$wikiPage, &$user, $limit, $reason ] );
2331 } else { // Protection of non-existing page (also known as "title protection")
2332 // Cascade protection is meaningless in this case
2333 $cascade = false;
2334
2335 if ( $limit['create'] != '' ) {
2336 $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
2337 $dbw->replace( 'protected_titles',
2338 [ [ 'pt_namespace', 'pt_title' ] ],
2339 [
2340 'pt_namespace' => $this->mTitle->getNamespace(),
2341 'pt_title' => $this->mTitle->getDBkey(),
2342 'pt_create_perm' => $limit['create'],
2343 'pt_timestamp' => $dbw->timestamp(),
2344 'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2345 'pt_user' => $user->getId(),
2346 ] + $commentFields, __METHOD__
2347 );
2348 $logParamsDetails[] = [
2349 'type' => 'create',
2350 'level' => $limit['create'],
2351 'expiry' => $expiry['create'],
2352 ];
2353 } else {
2354 $dbw->delete( 'protected_titles',
2355 [
2356 'pt_namespace' => $this->mTitle->getNamespace(),
2357 'pt_title' => $this->mTitle->getDBkey()
2358 ], __METHOD__
2359 );
2360 }
2361 }
2362
2363 $this->mTitle->flushRestrictions();
2364 InfoAction::invalidateCache( $this->mTitle );
2365
2366 if ( $logAction == 'unprotect' ) {
2367 $params = [];
2368 } else {
2369 $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2370 $params = [
2371 '4::description' => $protectDescriptionLog, // parameter for IRC
2372 '5:bool:cascade' => $cascade,
2373 'details' => $logParamsDetails, // parameter for localize and api
2374 ];
2375 }
2376
2377 // Update the protection log
2378 $logEntry = new ManualLogEntry( 'protect', $logAction );
2379 $logEntry->setTarget( $this->mTitle );
2380 $logEntry->setComment( $reason );
2381 $logEntry->setPerformer( $user );
2382 $logEntry->setParameters( $params );
2383 if ( !is_null( $nullRevision ) ) {
2384 $logEntry->setAssociatedRevId( $nullRevision->getId() );
2385 }
2386 $logEntry->setTags( $tags );
2387 if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2388 $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2389 }
2390 $logId = $logEntry->insert();
2391 $logEntry->publish( $logId );
2392
2393 return Status::newGood( $logId );
2394 }
2395
2407 public function insertProtectNullRevision( $revCommentMsg, array $limit,
2408 array $expiry, $cascade, $reason, $user = null
2409 ) {
2410 $dbw = wfGetDB( DB_MASTER );
2411
2412 // Prepare a null revision to be added to the history
2413 $editComment = wfMessage(
2414 $revCommentMsg,
2415 $this->mTitle->getPrefixedText(),
2416 $user ? $user->getName() : ''
2417 )->inContentLanguage()->text();
2418 if ( $reason ) {
2419 $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2420 }
2421 $protectDescription = $this->protectDescription( $limit, $expiry );
2422 if ( $protectDescription ) {
2423 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2424 $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2425 ->inContentLanguage()->text();
2426 }
2427 if ( $cascade ) {
2428 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2429 $editComment .= wfMessage( 'brackets' )->params(
2430 wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2431 )->inContentLanguage()->text();
2432 }
2433
2434 $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2435 if ( $nullRev ) {
2436 $nullRev->insertOn( $dbw );
2437
2438 // Update page record and touch page
2439 $oldLatest = $nullRev->getParentId();
2440 $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2441 }
2442
2443 return $nullRev;
2444 }
2445
2450 protected function formatExpiry( $expiry ) {
2451 if ( $expiry != 'infinity' ) {
2452 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2453 return wfMessage(
2454 'protect-expiring',
2455 $contLang->timeanddate( $expiry, false, false ),
2456 $contLang->date( $expiry, false, false ),
2457 $contLang->time( $expiry, false, false )
2458 )->inContentLanguage()->text();
2459 } else {
2460 return wfMessage( 'protect-expiry-indefinite' )
2461 ->inContentLanguage()->text();
2462 }
2463 }
2464
2472 public function protectDescription( array $limit, array $expiry ) {
2473 $protectDescription = '';
2474
2475 foreach ( array_filter( $limit ) as $action => $restrictions ) {
2476 # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2477 # All possible message keys are listed here for easier grepping:
2478 # * restriction-create
2479 # * restriction-edit
2480 # * restriction-move
2481 # * restriction-upload
2482 $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2483 # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2484 # with '' filtered out. All possible message keys are listed below:
2485 # * protect-level-autoconfirmed
2486 # * protect-level-sysop
2487 $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2488 ->inContentLanguage()->text();
2489
2490 $expiryText = $this->formatExpiry( $expiry[$action] );
2491
2492 if ( $protectDescription !== '' ) {
2493 $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2494 }
2495 $protectDescription .= wfMessage( 'protect-summary-desc' )
2496 ->params( $actionText, $restrictionsText, $expiryText )
2497 ->inContentLanguage()->text();
2498 }
2499
2500 return $protectDescription;
2501 }
2502
2514 public function protectDescriptionLog( array $limit, array $expiry ) {
2515 $protectDescriptionLog = '';
2516
2517 $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
2518 foreach ( array_filter( $limit ) as $action => $restrictions ) {
2519 $expiryText = $this->formatExpiry( $expiry[$action] );
2520 $protectDescriptionLog .=
2521 $dirMark .
2522 "[$action=$restrictions] ($expiryText)";
2523 }
2524
2525 return trim( $protectDescriptionLog );
2526 }
2527
2537 protected static function flattenRestrictions( $limit ) {
2538 if ( !is_array( $limit ) ) {
2539 throw new MWException( __METHOD__ . ' given non-array restriction set' );
2540 }
2541
2542 $bits = [];
2543 ksort( $limit );
2544
2545 foreach ( array_filter( $limit ) as $action => $restrictions ) {
2546 $bits[] = "$action=$restrictions";
2547 }
2548
2549 return implode( ':', $bits );
2550 }
2551
2564 public function isBatchedDelete( $safetyMargin = 0 ) {
2566
2567 $dbr = wfGetDB( DB_REPLICA );
2568 $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
2569 $revCount += $safetyMargin;
2570
2571 return $revCount >= $wgDeleteRevisionsBatchSize;
2572 }
2573
2593 public function doDeleteArticle(
2594 $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2595 $immediate = false
2596 ) {
2597 $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
2598 [], 'delete', $immediate );
2599
2600 // Returns true if the page was actually deleted, or is scheduled for deletion
2601 return $status->isOK();
2602 }
2603
2626 public function doDeleteArticleReal(
2627 $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
2628 $tags = [], $logsubtype = 'delete', $immediate = false
2629 ) {
2630 global $wgUser;
2631
2632 wfDebug( __METHOD__ . "\n" );
2633
2634 $status = Status::newGood();
2635
2636 // Avoid PHP 7.1 warning of passing $this by reference
2637 $wikiPage = $this;
2638
2639 if ( !$deleter ) {
2640 $deleter = $wgUser;
2641 }
2642 if ( !Hooks::run( 'ArticleDelete',
2643 [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
2644 ) ) {
2645 if ( $status->isOK() ) {
2646 // Hook aborted but didn't set a fatal status
2647 $status->fatal( 'delete-hook-aborted' );
2648 }
2649 return $status;
2650 }
2651
2652 return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
2653 $logsubtype, $immediate );
2654 }
2655
2664 public function doDeleteArticleBatched(
2665 $reason, $suppress, User $deleter, $tags,
2666 $logsubtype, $immediate = false, $webRequestId = null
2667 ) {
2668 wfDebug( __METHOD__ . "\n" );
2669
2670 $status = Status::newGood();
2671
2672 $dbw = wfGetDB( DB_MASTER );
2673 $dbw->startAtomic( __METHOD__ );
2674
2675 $this->loadPageData( self::READ_LATEST );
2676 $id = $this->getId();
2677 // T98706: lock the page from various other updates but avoid using
2678 // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2679 // the revisions queries (which also JOIN on user). Only lock the page
2680 // row and CAS check on page_latest to see if the trx snapshot matches.
2681 $lockedLatest = $this->lockAndGetLatest();
2682 if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2683 $dbw->endAtomic( __METHOD__ );
2684 // Page not there or trx snapshot is stale
2685 $status->error( 'cannotdelete',
2686 wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2687 return $status;
2688 }
2689
2690 // At this point we are now committed to returning an OK
2691 // status unless some DB query error or other exception comes up.
2692 // This way callers don't have to call rollback() if $status is bad
2693 // unless they actually try to catch exceptions (which is rare).
2694
2695 // we need to remember the old content so we can use it to generate all deletion updates.
2696 $revision = $this->getRevision();
2697 try {
2698 $content = $this->getContent( Revision::RAW );
2699 } catch ( Exception $ex ) {
2700 wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2701 . $ex->getMessage() );
2702
2703 $content = null;
2704 }
2705
2706 // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
2707 // one batch of revisions and defer archival of any others to the job queue.
2708 $explictTrxLogged = false;
2709 while ( true ) {
2710 $done = $this->archiveRevisions( $dbw, $id, $suppress );
2711 if ( $done || !$immediate ) {
2712 break;
2713 }
2714 $dbw->endAtomic( __METHOD__ );
2715 if ( $dbw->explicitTrxActive() ) {
2716 // Explict transactions may never happen here in practice. Log to be sure.
2717 if ( !$explictTrxLogged ) {
2718 $explictTrxLogged = true;
2719 LoggerFactory::getInstance( 'wfDebug' )->debug(
2720 'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
2721 'title' => $this->getTitle()->getText(),
2722 ] );
2723 }
2724 continue;
2725 }
2726 if ( $dbw->trxLevel() ) {
2727 $dbw->commit();
2728 }
2729 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
2730 $lbFactory->waitForReplication();
2731 $dbw->startAtomic( __METHOD__ );
2732 }
2733
2734 // If done archiving, also delete the article.
2735 if ( !$done ) {
2736 $dbw->endAtomic( __METHOD__ );
2737
2738 $jobParams = [
2739 'wikiPageId' => $id,
2740 'requestId' => $webRequestId ?? WebRequest::getRequestId(),
2741 'reason' => $reason,
2742 'suppress' => $suppress,
2743 'userId' => $deleter->getId(),
2744 'tags' => json_encode( $tags ),
2745 'logsubtype' => $logsubtype,
2746 ];
2747
2748 $job = new DeletePageJob( $this->getTitle(), $jobParams );
2749 JobQueueGroup::singleton()->push( $job );
2750
2751 $status->warning( 'delete-scheduled',
2752 wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2753 } else {
2754 // Get archivedRevisionCount by db query, because there's no better alternative.
2755 // Jobs cannot pass a count of archived revisions to the next job, because additional
2756 // deletion operations can be started while the first is running. Jobs from each
2757 // gracefully interleave, but would not know about each other's count. Deduplication
2758 // in the job queue to avoid simultaneous deletion operations would add overhead.
2759 // Number of archived revisions cannot be known beforehand, because edits can be made
2760 // while deletion operations are being processed, changing the number of archivals.
2761 $archivedRevisionCount = (int)$dbw->selectField(
2762 'archive', 'COUNT(*)',
2763 [
2764 'ar_namespace' => $this->getTitle()->getNamespace(),
2765 'ar_title' => $this->getTitle()->getDBkey(),
2766 'ar_page_id' => $id
2767 ], __METHOD__
2768 );
2769
2770 // Clone the title and wikiPage, so we have the information we need when
2771 // we log and run the ArticleDeleteComplete hook.
2772 $logTitle = clone $this->mTitle;
2773 $wikiPageBeforeDelete = clone $this;
2774
2775 // Now that it's safely backed up, delete it
2776 $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2777
2778 // Log the deletion, if the page was suppressed, put it in the suppression log instead
2779 $logtype = $suppress ? 'suppress' : 'delete';
2780
2781 $logEntry = new ManualLogEntry( $logtype, $logsubtype );
2782 $logEntry->setPerformer( $deleter );
2783 $logEntry->setTarget( $logTitle );
2784 $logEntry->setComment( $reason );
2785 $logEntry->setTags( $tags );
2786 $logid = $logEntry->insert();
2787
2788 $dbw->onTransactionPreCommitOrIdle(
2789 function () use ( $logEntry, $logid ) {
2790 // T58776: avoid deadlocks (especially from FileDeleteForm)
2791 $logEntry->publish( $logid );
2792 },
2793 __METHOD__
2794 );
2795
2796 $dbw->endAtomic( __METHOD__ );
2797
2798 $this->doDeleteUpdates( $id, $content, $revision, $deleter );
2799
2800 Hooks::run( 'ArticleDeleteComplete', [
2801 &$wikiPageBeforeDelete,
2802 &$deleter,
2803 $reason,
2804 $id,
2805 $content,
2806 $logEntry,
2807 $archivedRevisionCount
2808 ] );
2809 $status->value = $logid;
2810
2811 // Show log excerpt on 404 pages rather than just a link
2812 $cache = MediaWikiServices::getInstance()->getMainObjectStash();
2813 $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2814 $cache->set( $key, 1, $cache::TTL_DAY );
2815 }
2816
2817 return $status;
2818 }
2819
2829 protected function archiveRevisions( $dbw, $id, $suppress ) {
2832
2833 // Given the lock above, we can be confident in the title and page ID values
2834 $namespace = $this->getTitle()->getNamespace();
2835 $dbKey = $this->getTitle()->getDBkey();
2836
2837 $commentStore = CommentStore::getStore();
2838 $actorMigration = ActorMigration::newMigration();
2839
2841 $bitfield = false;
2842
2843 // Bitfields to further suppress the content
2844 if ( $suppress ) {
2845 $bitfield = Revision::SUPPRESSED_ALL;
2846 $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
2847 }
2848
2849 // For now, shunt the revision data into the archive table.
2850 // Text is *not* removed from the text table; bulk storage
2851 // is left intact to avoid breaking block-compression or
2852 // immutable storage schemes.
2853 // In the future, we may keep revisions and mark them with
2854 // the rev_deleted field, which is reserved for this purpose.
2855
2856 // Lock rows in `revision` and its temp tables, but not any others.
2857 // Note array_intersect() preserves keys from the first arg, and we're
2858 // assuming $revQuery has `revision` primary and isn't using subtables
2859 // for anything we care about.
2860 $dbw->lockForUpdate(
2861 array_intersect(
2862 $revQuery['tables'],
2863 [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
2864 ),
2865 [ 'rev_page' => $id ],
2866 __METHOD__,
2867 [],
2868 $revQuery['joins']
2869 );
2870
2871 // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
2872 // so we can copy it to the archive table.
2873 // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
2875 $revQuery['fields'][] = 'rev_text_id';
2876
2877 if ( $wgContentHandlerUseDB ) {
2878 $revQuery['fields'][] = 'rev_content_model';
2879 $revQuery['fields'][] = 'rev_content_format';
2880 }
2881 }
2882
2883 // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
2884 // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
2885 $res = $dbw->select(
2886 $revQuery['tables'],
2887 $revQuery['fields'],
2888 [ 'rev_page' => $id ],
2889 __METHOD__,
2890 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
2891 $revQuery['joins']
2892 );
2893
2894 // Build their equivalent archive rows
2895 $rowsInsert = [];
2896 $revids = [];
2897
2899 $ipRevIds = [];
2900
2901 $done = true;
2902 foreach ( $res as $row ) {
2903 if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
2904 $done = false;
2905 break;
2906 }
2907
2908 $comment = $commentStore->getComment( 'rev_comment', $row );
2909 $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
2910 $rowInsert = [
2911 'ar_namespace' => $namespace,
2912 'ar_title' => $dbKey,
2913 'ar_timestamp' => $row->rev_timestamp,
2914 'ar_minor_edit' => $row->rev_minor_edit,
2915 'ar_rev_id' => $row->rev_id,
2916 'ar_parent_id' => $row->rev_parent_id,
2925 'ar_len' => $row->rev_len,
2926 'ar_page_id' => $id,
2927 'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2928 'ar_sha1' => $row->rev_sha1,
2929 ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
2930 + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
2931
2933 $rowInsert['ar_text_id'] = $row->rev_text_id;
2934
2935 if ( $wgContentHandlerUseDB ) {
2936 $rowInsert['ar_content_model'] = $row->rev_content_model;
2937 $rowInsert['ar_content_format'] = $row->rev_content_format;
2938 }
2939 }
2940
2941 $rowsInsert[] = $rowInsert;
2942 $revids[] = $row->rev_id;
2943
2944 // Keep track of IP edits, so that the corresponding rows can
2945 // be deleted in the ip_changes table.
2946 if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
2947 $ipRevIds[] = $row->rev_id;
2948 }
2949 }
2950
2951 // This conditional is just a sanity check
2952 if ( count( $revids ) > 0 ) {
2953 // Copy them into the archive table
2954 $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2955
2956 $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
2957 $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
2959 $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
2960 }
2961
2962 // Also delete records from ip_changes as applicable.
2963 if ( count( $ipRevIds ) > 0 ) {
2964 $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2965 }
2966 }
2967
2968 return $done;
2969 }
2970
2977 public function lockAndGetLatest() {
2978 return (int)wfGetDB( DB_MASTER )->selectField(
2979 'page',
2980 'page_latest',
2981 [
2982 'page_id' => $this->getId(),
2983 // Typically page_id is enough, but some code might try to do
2984 // updates assuming the title is the same, so verify that
2985 'page_namespace' => $this->getTitle()->getNamespace(),
2986 'page_title' => $this->getTitle()->getDBkey()
2987 ],
2988 __METHOD__,
2989 [ 'FOR UPDATE' ]
2990 );
2991 }
2992
3005 public function doDeleteUpdates(
3006 $id, Content $content = null, Revision $revision = null, User $user = null
3007 ) {
3008 if ( $id !== $this->getId() ) {
3009 throw new InvalidArgumentException( 'Mismatching page ID' );
3010 }
3011
3012 try {
3013 $countable = $this->isCountable();
3014 } catch ( Exception $ex ) {
3015 // fallback for deleting broken pages for which we cannot load the content for
3016 // some reason. Note that doDeleteArticleReal() already logged this problem.
3017 $countable = false;
3018 }
3019
3020 // Update site status
3021 DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
3022 [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
3023 ) );
3024
3025 // Delete pagelinks, update secondary indexes, etc
3026 $updates = $this->getDeletionUpdates(
3027 $revision ? $revision->getRevisionRecord() : $content
3028 );
3029 foreach ( $updates as $update ) {
3030 DeferredUpdates::addUpdate( $update );
3031 }
3032
3033 $causeAgent = $user ? $user->getName() : 'unknown';
3034 // Reparse any pages transcluding this page
3035 LinksUpdate::queueRecursiveJobsForTable(
3036 $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
3037 // Reparse any pages including this image
3038 if ( $this->mTitle->getNamespace() == NS_FILE ) {
3039 LinksUpdate::queueRecursiveJobsForTable(
3040 $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
3041 }
3042
3043 // Clear caches
3044 self::onArticleDelete( $this->mTitle );
3045 ResourceLoaderWikiModule::invalidateModuleCache(
3046 $this->mTitle,
3047 $revision,
3048 null,
3049 WikiMap::getCurrentWikiDbDomain()->getId()
3050 );
3051
3052 // Reset this object and the Title object
3053 $this->loadFromRow( false, self::READ_LATEST );
3054
3055 // Search engine
3056 DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3057 }
3058
3088 public function doRollback(
3089 $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3090 ) {
3091 $resultDetails = null;
3092
3093 // Check permissions
3094 $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3095 $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3096 $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3097
3098 if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3099 $errors[] = [ 'sessionfailure' ];
3100 }
3101
3102 if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3103 $errors[] = [ 'actionthrottledtext' ];
3104 }
3105
3106 // If there were errors, bail out now
3107 if ( !empty( $errors ) ) {
3108 return $errors;
3109 }
3110
3111 return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3112 }
3113
3134 public function commitRollback( $fromP, $summary, $bot,
3135 &$resultDetails, User $guser, $tags = null
3136 ) {
3137 global $wgUseRCPatrol;
3138
3139 $dbw = wfGetDB( DB_MASTER );
3140
3141 if ( wfReadOnly() ) {
3142 return [ [ 'readonlytext' ] ];
3143 }
3144
3145 // Begin revision creation cycle by creating a PageUpdater.
3146 // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
3147 $updater = $this->newPageUpdater( $guser );
3148 $current = $updater->grabParentRevision();
3149
3150 if ( is_null( $current ) ) {
3151 // Something wrong... no page?
3152 return [ [ 'notanarticle' ] ];
3153 }
3154
3155 $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
3156 $legacyCurrent = new Revision( $current );
3157 $from = str_replace( '_', ' ', $fromP );
3158
3159 // User name given should match up with the top revision.
3160 // If the revision's user is not visible, then $from should be empty.
3161 if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
3162 $resultDetails = [ 'current' => $legacyCurrent ];
3163 return [ [ 'alreadyrolled',
3164 htmlspecialchars( $this->mTitle->getPrefixedText() ),
3165 htmlspecialchars( $fromP ),
3166 htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
3167 ] ];
3168 }
3169
3170 // Get the last edit not by this person...
3171 // Note: these may not be public values
3172 $actorWhere = ActorMigration::newMigration()->getWhere(
3173 $dbw,
3174 'rev_user',
3175 $current->getUser( RevisionRecord::RAW )
3176 );
3177
3178 $s = $dbw->selectRow(
3179 [ 'revision' ] + $actorWhere['tables'],
3180 [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3181 [
3182 'rev_page' => $current->getPageId(),
3183 'NOT(' . $actorWhere['conds'] . ')',
3184 ],
3185 __METHOD__,
3186 [
3187 'USE INDEX' => [ 'revision' => 'page_timestamp' ],
3188 'ORDER BY' => 'rev_timestamp DESC'
3189 ],
3190 $actorWhere['joins']
3191 );
3192 if ( $s === false ) {
3193 // No one else ever edited this page
3194 return [ [ 'cantrollback' ] ];
3195 } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
3196 || $s->rev_deleted & RevisionRecord::DELETED_USER
3197 ) {
3198 // Only admins can see this text
3199 return [ [ 'notvisiblerev' ] ];
3200 }
3201
3202 // Generate the edit summary if necessary
3203 $target = $this->getRevisionStore()->getRevisionById(
3204 $s->rev_id,
3205 RevisionStore::READ_LATEST
3206 );
3207 if ( empty( $summary ) ) {
3208 if ( !$currentEditorForPublic ) { // no public user name
3209 $summary = wfMessage( 'revertpage-nouser' );
3210 } else {
3211 $summary = wfMessage( 'revertpage' );
3212 }
3213 }
3214 $legacyTarget = new Revision( $target );
3215 $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
3216
3217 // Allow the custom summary to use the same args as the default message
3218 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3219 $args = [
3220 $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
3221 $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
3222 $s->rev_id,
3223 $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3224 $current->getId(),
3225 $contLang->timeanddate( $current->getTimestamp() )
3226 ];
3227 if ( $summary instanceof Message ) {
3228 $summary = $summary->params( $args )->inContentLanguage()->text();
3229 } else {
3230 $summary = wfMsgReplaceArgs( $summary, $args );
3231 }
3232
3233 // Trim spaces on user supplied text
3234 $summary = trim( $summary );
3235
3236 // Save
3237 $flags = EDIT_UPDATE | EDIT_INTERNAL;
3238
3239 if ( $guser->isAllowed( 'minoredit' ) ) {
3240 $flags |= EDIT_MINOR;
3241 }
3242
3243 if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3244 $flags |= EDIT_FORCE_BOT;
3245 }
3246
3247 // TODO: MCR: also log model changes in other slots, in case that becomes possible!
3248 $currentContent = $current->getContent( SlotRecord::MAIN );
3249 $targetContent = $target->getContent( SlotRecord::MAIN );
3250 $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
3251
3252 if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
3253 $tags[] = 'mw-rollback';
3254 }
3255
3256 // Build rollback revision:
3257 // Restore old content
3258 // TODO: MCR: test this once we can store multiple slots
3259 foreach ( $target->getSlots()->getSlots() as $slot ) {
3260 $updater->inheritSlot( $slot );
3261 }
3262
3263 // Remove extra slots
3264 // TODO: MCR: test this once we can store multiple slots
3265 foreach ( $current->getSlotRoles() as $role ) {
3266 if ( !$target->hasSlot( $role ) ) {
3267 $updater->removeSlot( $role );
3268 }
3269 }
3270
3271 $updater->setOriginalRevisionId( $target->getId() );
3272 // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
3273 $updater->addTags( $tags );
3274
3275 // TODO: this logic should not be in the storage layer, it's here for compatibility
3276 // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
3277 // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
3278 if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $guser ) ) {
3279 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
3280 }
3281
3282 // Actually store the rollback
3283 $rev = $updater->saveRevision(
3284 CommentStoreComment::newUnsavedComment( $summary ),
3285 $flags
3286 );
3287
3288 // Set patrolling and bot flag on the edits, which gets rollbacked.
3289 // This is done even on edit failure to have patrolling in that case (T64157).
3290 $set = [];
3291 if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3292 // Mark all reverted edits as bot
3293 $set['rc_bot'] = 1;
3294 }
3295
3296 if ( $wgUseRCPatrol ) {
3297 // Mark all reverted edits as patrolled
3298 $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
3299 }
3300
3301 if ( count( $set ) ) {
3302 $actorWhere = ActorMigration::newMigration()->getWhere(
3303 $dbw,
3304 'rc_user',
3305 $current->getUser( RevisionRecord::RAW ),
3306 false
3307 );
3308 $dbw->update( 'recentchanges', $set,
3309 [ /* WHERE */
3310 'rc_cur_id' => $current->getPageId(),
3311 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3312 $actorWhere['conds'], // No tables/joins are needed for rc_user
3313 ],
3314 __METHOD__
3315 );
3316 }
3317
3318 if ( !$updater->wasSuccessful() ) {
3319 return $updater->getStatus()->getErrorsArray();
3320 }
3321
3322 // Report if the edit was not created because it did not change the content.
3323 if ( $updater->isUnchanged() ) {
3324 $resultDetails = [ 'current' => $legacyCurrent ];
3325 return [ [ 'alreadyrolled',
3326 htmlspecialchars( $this->mTitle->getPrefixedText() ),
3327 htmlspecialchars( $fromP ),
3328 htmlspecialchars( $targetEditorForPublic ? $targetEditorForPublic->getName() : '' )
3329 ] ];
3330 }
3331
3332 if ( $changingContentModel ) {
3333 // If the content model changed during the rollback,
3334 // make sure it gets logged to Special:Log/contentmodel
3335 $log = new ManualLogEntry( 'contentmodel', 'change' );
3336 $log->setPerformer( $guser );
3337 $log->setTarget( $this->mTitle );
3338 $log->setComment( $summary );
3339 $log->setParameters( [
3340 '4::oldmodel' => $currentContent->getModel(),
3341 '5::newmodel' => $targetContent->getModel(),
3342 ] );
3343
3344 $logId = $log->insert( $dbw );
3345 $log->publish( $logId );
3346 }
3347
3348 $revId = $rev->getId();
3349
3350 Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
3351
3352 $resultDetails = [
3353 'summary' => $summary,
3354 'current' => $legacyCurrent,
3355 'target' => $legacyTarget,
3356 'newid' => $revId,
3357 'tags' => $tags
3358 ];
3359
3360 // TODO: make this return a Status object and wrap $resultDetails in that.
3361 return [];
3362 }
3363
3375 public static function onArticleCreate( Title $title ) {
3376 // TODO: move this into a PageEventEmitter service
3377
3378 // Update existence markers on article/talk tabs...
3379 $other = $title->getOtherPage();
3380
3381 $other->purgeSquid();
3382
3383 $title->touchLinks();
3384 $title->purgeSquid();
3385 $title->deleteTitleProtection();
3386
3387 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3388
3389 // Invalidate caches of articles which include this page
3390 DeferredUpdates::addUpdate(
3391 new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
3392 );
3393
3394 if ( $title->getNamespace() == NS_CATEGORY ) {
3395 // Load the Category object, which will schedule a job to create
3396 // the category table row if necessary. Checking a replica DB is ok
3397 // here, in the worst case it'll run an unnecessary recount job on
3398 // a category that probably doesn't have many members.
3399 Category::newFromTitle( $title )->getID();
3400 }
3401 }
3402
3408 public static function onArticleDelete( Title $title ) {
3409 // TODO: move this into a PageEventEmitter service
3410
3411 // Update existence markers on article/talk tabs...
3412 // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
3413 BacklinkCache::get( $title )->clear();
3414 $other = $title->getOtherPage();
3415
3416 $other->purgeSquid();
3417
3418 $title->touchLinks();
3419 $title->purgeSquid();
3420
3421 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3422
3423 // File cache
3426
3427 // Messages
3428 if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3429 MessageCache::singleton()->updateMessageOverride( $title, null );
3430 }
3431
3432 // Images
3433 if ( $title->getNamespace() == NS_FILE ) {
3434 DeferredUpdates::addUpdate(
3435 new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
3436 );
3437 }
3438
3439 // User talk pages
3440 if ( $title->getNamespace() == NS_USER_TALK ) {
3441 $user = User::newFromName( $title->getText(), false );
3442 if ( $user ) {
3443 $user->setNewtalk( false );
3444 }
3445 }
3446
3447 // Image redirects
3448 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3449
3450 // Purge cross-wiki cache entities referencing this page
3451 self::purgeInterwikiCheckKey( $title );
3452 }
3453
3462 public static function onArticleEdit(
3463 Title $title,
3464 Revision $revision = null,
3465 $slotsChanged = null
3466 ) {
3467 // TODO: move this into a PageEventEmitter service
3468
3469 if ( $slotsChanged === null || in_array( SlotRecord::MAIN, $slotsChanged ) ) {
3470 // Invalidate caches of articles which include this page.
3471 // Only for the main slot, because only the main slot is transcluded.
3472 // TODO: MCR: not true for TemplateStyles! [SlotHandler]
3473 DeferredUpdates::addUpdate(
3474 new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
3475 );
3476 }
3477
3478 // Invalidate the caches of all pages which redirect here
3479 DeferredUpdates::addUpdate(
3480 new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
3481 );
3482
3483 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3484
3485 // Purge CDN for this page only
3486 $title->purgeSquid();
3487 // Clear file cache for this page only
3489
3490 // Purge ?action=info cache
3491 $revid = $revision ? $revision->getId() : null;
3492 DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
3494 } );
3495
3496 // Purge cross-wiki cache entities referencing this page
3497 self::purgeInterwikiCheckKey( $title );
3498 }
3499
3507 private static function purgeInterwikiCheckKey( Title $title ) {
3509
3511 return; // @todo: perhaps this wiki is only used as a *source* for content?
3512 }
3513
3514 DeferredUpdates::addCallableUpdate( function () use ( $title ) {
3515 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3516 $cache->resetCheckKey(
3517 // Do not include the namespace since there can be multiple aliases to it
3518 // due to different namespace text definitions on different wikis. This only
3519 // means that some cache invalidations happen that are not strictly needed.
3520 $cache->makeGlobalKey(
3521 'interwiki-page',
3522 WikiMap::getCurrentWikiDbDomain()->getId(),
3523 $title->getDBkey()
3524 )
3525 );
3526 } );
3527 }
3528
3535 public function getCategories() {
3536 $id = $this->getId();
3537 if ( $id == 0 ) {
3538 return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3539 }
3540
3541 $dbr = wfGetDB( DB_REPLICA );
3542 $res = $dbr->select( 'categorylinks',
3543 [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3544 // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3545 // as not being aliases, and NS_CATEGORY is numeric
3546 [ 'cl_from' => $id ],
3547 __METHOD__ );
3548
3550 }
3551
3558 public function getHiddenCategories() {
3559 $result = [];
3560 $id = $this->getId();
3561
3562 if ( $id == 0 ) {
3563 return [];
3564 }
3565
3566 $dbr = wfGetDB( DB_REPLICA );
3567 $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3568 [ 'cl_to' ],
3569 [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3570 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3571 __METHOD__ );
3572
3573 if ( $res !== false ) {
3574 foreach ( $res as $row ) {
3575 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3576 }
3577 }
3578
3579 return $result;
3580 }
3581
3589 public function getAutoDeleteReason( &$hasHistory ) {
3590 return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3591 }
3592
3603 public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3604 $id = $id ?: $this->getId();
3605 $type = MWNamespace::getCategoryLinkType( $this->getTitle()->getNamespace() );
3606
3607 $addFields = [ 'cat_pages = cat_pages + 1' ];
3608 $removeFields = [ 'cat_pages = cat_pages - 1' ];
3609 if ( $type !== 'page' ) {
3610 $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
3611 $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
3612 }
3613
3614 $dbw = wfGetDB( DB_MASTER );
3615
3616 if ( count( $added ) ) {
3617 $existingAdded = $dbw->selectFieldValues(
3618 'category',
3619 'cat_title',
3620 [ 'cat_title' => $added ],
3621 __METHOD__
3622 );
3623
3624 // For category rows that already exist, do a plain
3625 // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3626 // to avoid creating gaps in the cat_id sequence.
3627 if ( count( $existingAdded ) ) {
3628 $dbw->update(
3629 'category',
3630 $addFields,
3631 [ 'cat_title' => $existingAdded ],
3632 __METHOD__
3633 );
3634 }
3635
3636 $missingAdded = array_diff( $added, $existingAdded );
3637 if ( count( $missingAdded ) ) {
3638 $insertRows = [];
3639 foreach ( $missingAdded as $cat ) {
3640 $insertRows[] = [
3641 'cat_title' => $cat,
3642 'cat_pages' => 1,
3643 'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
3644 'cat_files' => ( $type === 'file' ) ? 1 : 0,
3645 ];
3646 }
3647 $dbw->upsert(
3648 'category',
3649 $insertRows,
3650 [ 'cat_title' ],
3651 $addFields,
3652 __METHOD__
3653 );
3654 }
3655 }
3656
3657 if ( count( $deleted ) ) {
3658 $dbw->update(
3659 'category',
3660 $removeFields,
3661 [ 'cat_title' => $deleted ],
3662 __METHOD__
3663 );
3664 }
3665
3666 foreach ( $added as $catName ) {
3667 $cat = Category::newFromName( $catName );
3668 Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3669 }
3670
3671 foreach ( $deleted as $catName ) {
3672 $cat = Category::newFromName( $catName );
3673 Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3674 // Refresh counts on categories that should be empty now (after commit, T166757)
3675 DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
3676 $cat->refreshCountsIfEmpty();
3677 } );
3678 }
3679 }
3680
3687 public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3688 if ( wfReadOnly() ) {
3689 return;
3690 }
3691
3692 if ( !Hooks::run( 'OpportunisticLinksUpdate',
3693 [ $this, $this->mTitle, $parserOutput ]
3694 ) ) {
3695 return;
3696 }
3697
3698 $config = RequestContext::getMain()->getConfig();
3699
3700 $params = [
3701 'isOpportunistic' => true,
3702 'rootJobTimestamp' => $parserOutput->getCacheTime()
3703 ];
3704
3705 if ( $this->mTitle->areRestrictionsCascading() ) {
3706 // If the page is cascade protecting, the links should really be up-to-date
3707 JobQueueGroup::singleton()->lazyPush(
3709 );
3710 } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3711 // Assume the output contains "dynamic" time/random based magic words.
3712 // Only update pages that expired due to dynamic content and NOT due to edits
3713 // to referenced templates/files. When the cache expires due to dynamic content,
3714 // page_touched is unchanged. We want to avoid triggering redundant jobs due to
3715 // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3716 // template/file edit already triggered recursive RefreshLinksJob jobs.
3717 if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3718 // If a page is uncacheable, do not keep spamming a job for it.
3719 // Although it would be de-duplicated, it would still waste I/O.
3720 $cache = ObjectCache::getLocalClusterInstance();
3721 $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3722 $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3723 if ( $cache->add( $key, time(), $ttl ) ) {
3724 JobQueueGroup::singleton()->lazyPush(
3725 RefreshLinksJob::newDynamic( $this->mTitle, $params )
3726 );
3727 }
3728 }
3729 }
3730 }
3731
3741 public function getDeletionUpdates( $rev = null ) {
3742 if ( !$rev ) {
3743 wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' );
3744
3745 try {
3746 $rev = $this->getRevisionRecord();
3747 } catch ( Exception $ex ) {
3748 // If we can't load the content, something is wrong. Perhaps that's why
3749 // the user is trying to delete the page, so let's not fail in that case.
3750 // Note that doDeleteArticleReal() will already have logged an issue with
3751 // loading the content.
3752 wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() );
3753 }
3754 }
3755
3756 if ( !$rev ) {
3757 $slotContent = [];
3758 } elseif ( $rev instanceof Content ) {
3759 wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' );
3760
3761 $slotContent = [ SlotRecord::MAIN => $rev ];
3762 } else {
3763 $slotContent = array_map( function ( SlotRecord $slot ) {
3764 return $slot->getContent( Revision::RAW );
3765 }, $rev->getSlots()->getSlots() );
3766 }
3767
3768 $allUpdates = [ new LinksDeletionUpdate( $this ) ];
3769
3770 // NOTE: once Content::getDeletionUpdates() is removed, we only need to content
3771 // model here, not the content object!
3772 // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
3774 foreach ( $slotContent as $role => $content ) {
3775 $handler = $content->getContentHandler();
3776
3777 $updates = $handler->getDeletionUpdates(
3778 $this->getTitle(),
3779 $role
3780 );
3781 $allUpdates = array_merge( $allUpdates, $updates );
3782
3783 // TODO: remove B/C hack in 1.32!
3784 $legacyUpdates = $content->getDeletionUpdates( $this );
3785
3786 // HACK: filter out redundant and incomplete LinksDeletionUpdate
3787 $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
3788 return !( $update instanceof LinksDeletionUpdate );
3789 } );
3790
3791 $allUpdates = array_merge( $allUpdates, $legacyUpdates );
3792 }
3793
3794 Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] );
3795
3796 // TODO: hard deprecate old hook in 1.33
3797 Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] );
3798 return $allUpdates;
3799 }
3800
3808 public function isLocal() {
3809 return true;
3810 }
3811
3821 public function getWikiDisplayName() {
3822 global $wgSitename;
3823 return $wgSitename;
3824 }
3825
3834 public function getSourceURL() {
3835 return $this->getTitle()->getCanonicalURL();
3836 }
3837
3844 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3845
3846 return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
3847 }
3848
3849}
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.
$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 in the domain of [0, 1), in a way not likely to give duplicate values for ...
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.
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)
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
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:441
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 registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
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:61
getContentHandler()
Returns the content handler appropriate for this revision's content model.
Definition Revision.php:987
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:156
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition Revision.php:295
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:511
getTimestamp()
Definition Revision.php:994
getRevisionRecord()
Definition Revision.php:629
const FOR_PUBLIC
Definition Revision.php:54
const SUPPRESSED_ALL
Definition Revision.php:51
const RAW
Definition Revision.php:56
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:118
Database independant search index updater.
static newFromResult( $res)
Represents a title within MediaWiki.
Definition Title.php:40
getNamespace()
Get the namespace index, i.e.
Definition Title.php:994
getFragment()
Get the Title fragment (i.e.
Definition Title.php:1549
getDBkey()
Get the main part with underscores.
Definition Title.php:970
getInterwiki()
Get the interwiki prefix.
Definition Title.php:880
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:585
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition User.php:3856
getId()
Get the user's ID.
Definition User.php:2425
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:676
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:609
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition User.php:652
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:45
static newFromID( $id, $from='fromdb')
Constructor from a page id.
Definition WikiPage.php:176
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:530
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:138
pageDataFromTitle( $dbr, $title, $options=[])
Fetch a page record matching the Title object's namespace and title using a sanitized title string.
Definition WikiPage.php:455
getTimestamp()
Definition WikiPage.php:827
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:783
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:699
getMinorEdit()
Returns true if last revision was marked as "minor edit".
Definition WikiPage.php:925
clearCacheFields()
Clear the object cache fields.
Definition WikiPage.php:313
getRevisionRenderer()
Definition WikiPage.php:242
Revision $mLastRevision
Definition WikiPage.php:92
clearPreparedEdit()
Clear the mPreparedEdit cache field, as may be needed by mutable content types.
Definition WikiPage.php:333
getLatest()
Get the page_latest field.
Definition WikiPage.php:710
formatExpiry( $expiry)
getRevisionStore()
Definition WikiPage.php:235
PreparedEdit $mPreparedEdit
Map of cache fields (text, parser output, ect) for a proposed/new edit.
Definition WikiPage.php:72
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:77
__clone()
Makes sure that the mTitle object is cloned to the newly cloned WikiPage.
Definition WikiPage.php:126
loadFromRow( $data, $from)
Load the object from a database row.
Definition WikiPage.php:556
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:995
DerivedPageDataUpdater null $derivedDataUpdater
Definition WikiPage.php:112
setTimestamp( $ts)
Set the page timestamp (use only to avoid DB queries)
Definition WikiPage.php:841
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:854
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:422
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:721
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:102
setLastEdit(Revision $revision)
Set the latest revision.
Definition WikiPage.php:774
getDBLoadBalancer()
Definition WikiPage.php:263
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:734
Title $mTitle
Definition WikiPage.php:51
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:892
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition WikiPage.php:648
pageDataFromId( $dbr, $id, $options=[])
Fetch a page record matching the requested ID.
Definition WikiPage.php:469
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:97
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:911
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:206
getAutoDeleteReason(&$hasHistory)
Auto-generates a deletion reason.
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
int false $mLatest
False means "not loaded".
Definition WikiPage.php:69
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:621
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:816
getActionOverrides()
Definition WikiPage.php:273
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:383
int $mDataLoadedFrom
One of the READ_* constants.
Definition WikiPage.php:82
bool $mDataLoaded
Definition WikiPage.php:57
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Title $mRedirectTarget
Definition WikiPage.php:87
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:344
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:294
isRedirect()
Tests if the article content represents a redirect.
Definition WikiPage.php:630
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:107
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:485
getSlotRoleRegistry()
Definition WikiPage.php:249
clear()
Clear the object.
Definition WikiPage.php:302
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition WikiPage.php:677
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:942
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:286
getRevisionRecord()
Get the latest revision.
Definition WikiPage.php:795
getWikiDisplayName()
The display name for the site this content come from.
bool $mIsRedirect
Definition WikiPage.php:63
getParserCache()
Definition WikiPage.php:256
static convertSelectType( $type)
Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
Definition WikiPage.php:218
getCreator( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the User object of the user who created the page.
Definition WikiPage.php:873
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:688
__construct(Title $title)
Constructor and clear the article.
Definition WikiPage.php:118
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
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
const EDIT_FORCE_BOT
Definition Defines.php:165
const EDIT_INTERNAL
Definition Defines.php:168
const EDIT_UPDATE
Definition Defines.php:162
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:293
const NS_FILE
Definition Defines.php:79
const NS_MEDIAWIKI
Definition Defines.php:81
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:295
const NS_MEDIA
Definition Defines.php:61
const NS_USER_TALK
Definition Defines.php:76
const EDIT_MINOR
Definition Defines.php:163
const NS_CATEGORY
Definition Defines.php:87
const EDIT_NEW
Definition Defines.php:161
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. '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 '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:1991
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password 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:894
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:1266
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:1999
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:2848
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
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:996
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition hooks.txt:783
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:2003
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
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
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:1779
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