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