MediaWiki REL1_33
DerivedPageDataUpdater.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Storage;
24
34use InvalidArgumentException;
39use LogicException;
60use User;
61use Wikimedia\Assert\Assert;
64
97
101 private $user = null;
102
106 private $wikiPage;
107
112
117
121 private $contLang;
122
127
132
137
142
147
152 private $options = [
153 'changed' => true,
154 // newrev is true if prepareUpdate is handling the creation of a new revision,
155 // as opposed to a null edit or a forced update.
156 'newrev' => false,
157 'created' => false,
158 'moved' => false,
159 'restored' => false,
160 'oldrevision' => null,
161 'oldcountable' => null,
162 'oldredirect' => null,
163 'triggeringUser' => null,
164 // causeAction/causeAgent default to 'unknown' but that's handled where it's read,
165 // to make the life of prepareUpdate() callers easier.
166 'causeAction' => null,
167 'causeAgent' => null,
168 ];
169
189 private $pageState = null;
190
194 private $slotsUpdate = null;
195
199 private $parentRevision = null;
200
204 private $revision = null;
205
209 private $renderedRevision = null;
210
215
218
227 private $stage = 'new';
228
239 private static $transitions = [
240 'new' => [
241 'new' => true,
242 'knows-current' => true,
243 'has-content' => true,
244 'has-revision' => true,
245 ],
246 'knows-current' => [
247 'knows-current' => true,
248 'has-content' => true,
249 'has-revision' => true,
250 ],
251 'has-content' => [
252 'has-content' => true,
253 'has-revision' => true,
254 ],
255 'has-revision' => [
256 'has-revision' => true,
257 'done' => true,
258 ],
259 ];
260
272 public function __construct(
282 ) {
283 $this->wikiPage = $wikiPage;
284
285 $this->parserCache = $parserCache;
286 $this->revisionStore = $revisionStore;
287 $this->revisionRenderer = $revisionRenderer;
288 $this->slotRoleRegistry = $slotRoleRegistry;
289 $this->jobQueueGroup = $jobQueueGroup;
290 $this->messageCache = $messageCache;
291 $this->contLang = $contLang;
292 // XXX only needed for waiting for replicas to catch up; there should be a narrower
293 // interface for that.
294 $this->loadbalancerFactory = $loadbalancerFactory;
295 }
296
308 private function doTransition( $newStage ) {
309 $this->assertTransition( $newStage );
310
311 $oldStage = $this->stage;
312 $this->stage = $newStage;
313
314 return $oldStage;
315 }
316
326 private function assertTransition( $newStage ) {
327 if ( empty( self::$transitions[$this->stage][$newStage] ) ) {
328 throw new LogicException( "Cannot transition from {$this->stage} to $newStage" );
329 }
330 }
331
335 private function getWikiId() {
336 // TODO: get from RevisionStore
337 return false;
338 }
339
351 public function isReusableFor(
352 UserIdentity $user = null,
355 $parentId = null
356 ) {
357 if ( $revision
358 && $parentId
359 && $revision->getParentId() !== $parentId
360 ) {
361 throw new InvalidArgumentException( '$parentId should match the parent of $revision' );
362 }
363
364 // NOTE: For null revisions, $user may be different from $this->revision->getUser
365 // and also from $revision->getUser.
366 // But $user should always match $this->user.
367 if ( $user && $this->user && $user->getName() !== $this->user->getName() ) {
368 return false;
369 }
370
371 if ( $revision && $this->revision && $this->revision->getId()
372 && $this->revision->getId() !== $revision->getId()
373 ) {
374 return false;
375 }
376
377 if ( $this->pageState
378 && $revision
379 && $revision->getParentId() !== null
380 && $this->pageState['oldId'] !== $revision->getParentId()
381 ) {
382 return false;
383 }
384
385 if ( $this->pageState
386 && $parentId !== null
387 && $this->pageState['oldId'] !== $parentId
388 ) {
389 return false;
390 }
391
392 // NOTE: this check is the primary reason for having the $this->slotsUpdate field!
393 if ( $this->slotsUpdate
394 && $slotsUpdate
395 && !$this->slotsUpdate->hasSameUpdates( $slotsUpdate )
396 ) {
397 return false;
398 }
399
400 if ( $revision
401 && $this->revision
402 && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
403 ) {
404 return false;
405 }
406
407 return true;
408 }
409
415 $this->articleCountMethod = $articleCountMethod;
416 }
417
423 $this->rcWatchCategoryMembership = $rcWatchCategoryMembership;
424 }
425
429 private function getTitle() {
430 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
431 return $this->wikiPage->getTitle();
432 }
433
437 private function getWikiPage() {
438 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
439 return $this->wikiPage;
440 }
441
449 public function pageExisted() {
450 $this->assertHasPageState( __METHOD__ );
451
452 return $this->pageState['oldId'] > 0;
453 }
454
464 private function getParentRevision() {
465 $this->assertPrepared( __METHOD__ );
466
467 if ( $this->parentRevision ) {
469 }
470
471 if ( !$this->pageState['oldId'] ) {
472 // If there was no current revision, there is no parent revision,
473 // since the page didn't exist.
474 return null;
475 }
476
477 $oldId = $this->revision->getParentId();
478 $flags = $this->useMaster() ? RevisionStore::READ_LATEST : 0;
479 $this->parentRevision = $oldId
480 ? $this->revisionStore->getRevisionById( $oldId, $flags )
481 : null;
482
484 }
485
506 public function grabCurrentRevision() {
507 if ( $this->pageState ) {
508 return $this->pageState['oldRevision'];
509 }
510
511 $this->assertTransition( 'knows-current' );
512
513 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
514 $wikiPage = $this->getWikiPage();
515
516 // Do not call WikiPage::clear(), since the caller may already have caused page data
517 // to be loaded with SELECT FOR UPDATE. Just assert it's loaded now.
518 $wikiPage->loadPageData( self::READ_LATEST );
520 $current = $rev ? $rev->getRevisionRecord() : null;
521
522 $this->pageState = [
523 'oldRevision' => $current,
524 'oldId' => $rev ? $rev->getId() : 0,
525 'oldIsRedirect' => $wikiPage->isRedirect(), // NOTE: uses page table
526 'oldCountable' => $wikiPage->isCountable(), // NOTE: uses pagelinks table
527 ];
528
529 $this->doTransition( 'knows-current' );
530
531 return $this->pageState['oldRevision'];
532 }
533
539 public function isContentPrepared() {
540 return $this->revision !== null;
541 }
542
550 public function isUpdatePrepared() {
551 return $this->revision !== null && $this->revision->getId() !== null;
552 }
553
557 private function getPageId() {
558 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
559 return $this->wikiPage->getId();
560 }
561
567 public function isContentDeleted() {
568 if ( $this->revision ) {
569 // XXX: if that revision is the current revision, this should be skipped
570 return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
571 } else {
572 // If the content has not been saved yet, it cannot have been deleted yet.
573 return false;
574 }
575 }
576
586 public function getRawSlot( $role ) {
587 return $this->getSlots()->getSlot( $role );
588 }
589
598 public function getRawContent( $role ) {
599 return $this->getRawSlot( $role )->getContent();
600 }
601
608 private function getContentModel( $role ) {
609 return $this->getRawSlot( $role )->getModel();
610 }
611
616 private function getContentHandler( $role ) {
617 // TODO: inject something like a ContentHandlerRegistry
618 return ContentHandler::getForModelID( $this->getContentModel( $role ) );
619 }
620
621 private function useMaster() {
622 // TODO: can we just set a flag to true in prepareContent()?
623 return $this->wikiPage->wasLoadedFrom( self::READ_LATEST );
624 }
625
629 public function isCountable() {
630 // NOTE: Keep in sync with WikiPage::isCountable.
631
632 if ( !$this->getTitle()->isContentPage() ) {
633 return false;
634 }
635
636 if ( $this->isContentDeleted() ) {
637 // This should be irrelevant: countability only applies to the current revision,
638 // and the current revision is never suppressed.
639 return false;
640 }
641
642 if ( $this->isRedirect() ) {
643 return false;
644 }
645
646 $hasLinks = null;
647
648 if ( $this->articleCountMethod === 'link' ) {
649 // NOTE: it would be more appropriate to determine for each slot separately
650 // whether it has links, and use that information with that slot's
651 // isCountable() method. However, that would break parity with
652 // WikiPage::isCountable, which uses the pagelinks table to determine
653 // whether the current revision has links.
654 $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() );
655 }
656
657 foreach ( $this->getModifiedSlotRoles() as $role ) {
658 $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
659 if ( $roleHandler->supportsArticleCount() ) {
660 $content = $this->getRawContent( $role );
661
662 if ( $content->isCountable( $hasLinks ) ) {
663 return true;
664 }
665 }
666 }
667
668 return false;
669 }
670
674 public function isRedirect() {
675 // NOTE: main slot determines redirect status
676 // TODO: MCR: this should be controlled by a PageTypeHandler
677 $mainContent = $this->getRawContent( SlotRecord::MAIN );
678
679 return $mainContent->isRedirect();
680 }
681
688 // NOTE: main slot determines redirect status
689 $mainContent = $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
690
691 return $mainContent->isRedirect();
692 }
693
718 public function prepareContent(
719 User $user,
721 $useStash = true
722 ) {
723 if ( $this->slotsUpdate ) {
724 if ( !$this->user ) {
725 throw new LogicException(
726 'Unexpected state: $this->slotsUpdate was initialized, '
727 . 'but $this->user was not.'
728 );
729 }
730
731 if ( $this->user->getName() !== $user->getName() ) {
732 throw new LogicException( 'Can\'t call prepareContent() again for different user! '
733 . 'Expected ' . $this->user->getName() . ', got ' . $user->getName()
734 );
735 }
736
737 if ( !$this->slotsUpdate->hasSameUpdates( $slotsUpdate ) ) {
738 throw new LogicException(
739 'Can\'t call prepareContent() again with different slot content!'
740 );
741 }
742
743 return; // prepareContent() already done, nothing to do
744 }
745
746 $this->assertTransition( 'has-content' );
747
748 $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
749 $title = $this->getTitle();
750
752
753 $this->slotsOutput = [];
754 $this->canonicalParserOutput = null;
755
756 // The edit may have already been prepared via api.php?action=stashedit
757 $stashedEdit = false;
758
759 // TODO: MCR: allow output for all slots to be stashed.
760 if ( $useStash && $slotsUpdate->isModifiedSlot( SlotRecord::MAIN ) ) {
761 $mainContent = $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent();
762 $legacyUser = User::newFromIdentity( $user );
763 $stashedEdit = ApiStashEdit::checkCache( $title, $mainContent, $legacyUser );
764 }
765
766 $userPopts = ParserOptions::newFromUserAndLang( $user, $this->contLang );
767 Hooks::run( 'ArticlePrepareTextForEdit', [ $wikiPage, $userPopts ] );
768
769 $this->user = $user;
770 $this->slotsUpdate = $slotsUpdate;
771
772 if ( $parentRevision ) {
773 $this->revision = MutableRevisionRecord::newFromParentRevision( $parentRevision );
774 } else {
775 $this->revision = new MutableRevisionRecord( $title );
776 }
777
778 // NOTE: user and timestamp must be set, so they can be used for
779 // {{subst:REVISIONUSER}} and {{subst:REVISIONTIMESTAMP}} in PST!
780 $this->revision->setTimestamp( wfTimestampNow() );
781 $this->revision->setUser( $user );
782
783 // Set up ParserOptions to operate on the new revision
784 $oldCallback = $userPopts->getCurrentRevisionCallback();
785 $userPopts->setCurrentRevisionCallback(
786 function ( Title $parserTitle, $parser = false ) use ( $title, $oldCallback ) {
787 if ( $parserTitle->equals( $title ) ) {
788 $legacyRevision = new Revision( $this->revision );
789 return $legacyRevision;
790 } else {
791 return call_user_func( $oldCallback, $parserTitle, $parser );
792 }
793 }
794 );
795
796 $pstContentSlots = $this->revision->getSlots();
797
798 foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
799 $slot = $slotsUpdate->getModifiedSlot( $role );
800
801 if ( $slot->isInherited() ) {
802 // No PST for inherited slots! Note that "modified" slots may still be inherited
803 // from an earlier version, e.g. for rollbacks.
804 $pstSlot = $slot;
805 } elseif ( $role === SlotRecord::MAIN && $stashedEdit ) {
806 // TODO: MCR: allow PST content for all slots to be stashed.
807 $pstSlot = SlotRecord::newUnsaved( $role, $stashedEdit->pstContent );
808 } else {
809 $content = $slot->getContent();
810 $pstContent = $content->preSaveTransform( $title, $this->user, $userPopts );
811 $pstSlot = SlotRecord::newUnsaved( $role, $pstContent );
812 }
813
814 $pstContentSlots->setSlot( $pstSlot );
815 }
816
817 foreach ( $slotsUpdate->getRemovedRoles() as $role ) {
818 $pstContentSlots->removeSlot( $role );
819 }
820
821 $this->options['created'] = ( $parentRevision === null );
822 $this->options['changed'] = ( $parentRevision === null
823 || !$pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
824
825 $this->doTransition( 'has-content' );
826
827 if ( !$this->options['changed'] ) {
828 // null-edit!
829
830 // TODO: move this into MutableRevisionRecord
831 // TODO: This needs to behave differently for a forced dummy edit!
832 $this->revision->setId( $parentRevision->getId() );
833 $this->revision->setTimestamp( $parentRevision->getTimestamp() );
834 $this->revision->setPageId( $parentRevision->getPageId() );
835 $this->revision->setParentId( $parentRevision->getParentId() );
836 $this->revision->setUser( $parentRevision->getUser( RevisionRecord::RAW ) );
837 $this->revision->setComment( $parentRevision->getComment( RevisionRecord::RAW ) );
838 $this->revision->setMinorEdit( $parentRevision->isMinor() );
839 $this->revision->setVisibility( $parentRevision->getVisibility() );
840
841 // prepareUpdate() is redundant for null-edits
842 $this->doTransition( 'has-revision' );
843 } else {
844 $this->parentRevision = $parentRevision;
845 }
846
847 $renderHints = [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ];
848
849 if ( $stashedEdit ) {
851 $output = $stashedEdit->output;
852
853 // TODO: this should happen when stashing the ParserOutput, not now!
854 $output->setCacheTime( $stashedEdit->timestamp );
855
856 $renderHints['known-revision-output'] = $output;
857 }
858
859 // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
860 // NOTE: the revision is either new or current, so we can bypass audience checks.
861 $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
862 $this->revision,
863 null,
864 null,
865 $renderHints
866 );
867 }
868
884 public function getRevision() {
885 $this->assertPrepared( __METHOD__ );
886 return $this->revision;
887 }
888
892 public function getRenderedRevision() {
893 $this->assertPrepared( __METHOD__ );
894
896 }
897
898 private function assertHasPageState( $method ) {
899 if ( !$this->pageState ) {
900 throw new LogicException(
901 'Must call grabCurrentRevision() or prepareContent() '
902 . 'or prepareUpdate() before calling ' . $method
903 );
904 }
905 }
906
907 private function assertPrepared( $method ) {
908 if ( !$this->revision ) {
909 throw new LogicException(
910 'Must call prepareContent() or prepareUpdate() before calling ' . $method
911 );
912 }
913 }
914
915 private function assertHasRevision( $method ) {
916 if ( !$this->revision->getId() ) {
917 throw new LogicException(
918 'Must call prepareUpdate() before calling ' . $method
919 );
920 }
921 }
922
928 public function isCreation() {
929 $this->assertPrepared( __METHOD__ );
930 return $this->options['created'];
931 }
932
942 public function isChange() {
943 $this->assertPrepared( __METHOD__ );
944 return $this->options['changed'];
945 }
946
952 public function wasRedirect() {
953 $this->assertHasPageState( __METHOD__ );
954
955 if ( $this->pageState['oldIsRedirect'] === null ) {
957 $rev = $this->pageState['oldRevision'];
958 if ( $rev ) {
959 $this->pageState['oldIsRedirect'] = $this->revisionIsRedirect( $rev );
960 } else {
961 $this->pageState['oldIsRedirect'] = false;
962 }
963 }
964
965 return $this->pageState['oldIsRedirect'];
966 }
967
976 public function getSlots() {
977 $this->assertPrepared( __METHOD__ );
978 return $this->revision->getSlots();
979 }
980
986 private function getRevisionSlotsUpdate() {
987 $this->assertPrepared( __METHOD__ );
988
989 if ( !$this->slotsUpdate ) {
990 $old = $this->getParentRevision();
992 $this->revision->getSlots(),
993 $old ? $old->getSlots() : null
994 );
995 }
996 return $this->slotsUpdate;
997 }
998
1005 public function getTouchedSlotRoles() {
1006 return $this->getRevisionSlotsUpdate()->getTouchedRoles();
1007 }
1008
1015 public function getModifiedSlotRoles() {
1016 return $this->getRevisionSlotsUpdate()->getModifiedRoles();
1017 }
1018
1024 public function getRemovedSlotRoles() {
1025 return $this->getRevisionSlotsUpdate()->getRemovedRoles();
1026 }
1027
1072 Assert::parameter(
1073 !isset( $options['oldrevision'] )
1074 || $options['oldrevision'] instanceof Revision
1075 || $options['oldrevision'] instanceof RevisionRecord,
1076 '$options["oldrevision"]',
1077 'must be a RevisionRecord (or Revision)'
1078 );
1079 Assert::parameter(
1080 !isset( $options['triggeringUser'] )
1081 || $options['triggeringUser'] instanceof UserIdentity,
1082 '$options["triggeringUser"]',
1083 'must be a UserIdentity'
1084 );
1085
1086 if ( !$revision->getId() ) {
1087 throw new InvalidArgumentException(
1088 'Revision must have an ID set for it to be used with prepareUpdate()!'
1089 );
1090 }
1091
1092 if ( $this->revision && $this->revision->getId() ) {
1093 if ( $this->revision->getId() === $revision->getId() ) {
1094 return; // nothing to do!
1095 } else {
1096 throw new LogicException(
1097 'Trying to re-use DerivedPageDataUpdater with revision '
1098 . $revision->getId()
1099 . ', but it\'s already bound to revision '
1100 . $this->revision->getId()
1101 );
1102 }
1103 }
1104
1105 if ( $this->revision
1106 && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
1107 ) {
1108 throw new LogicException(
1109 'The Revision provided has mismatching content!'
1110 );
1111 }
1112
1113 // Override fields defined in $this->options with values from $options.
1114 $this->options = array_intersect_key( $options, $this->options ) + $this->options;
1115
1116 if ( $this->revision ) {
1117 $oldId = $this->pageState['oldId'] ?? 0;
1118 $this->options['newrev'] = ( $revision->getId() !== $oldId );
1119 } elseif ( isset( $this->options['oldrevision'] ) ) {
1121 $oldRev = $this->options['oldrevision'];
1122 $oldId = $oldRev->getId();
1123 $this->options['newrev'] = ( $revision->getId() !== $oldId );
1124 } else {
1125 $oldId = $revision->getParentId();
1126 }
1127
1128 if ( $oldId !== null ) {
1129 // XXX: what if $options['changed'] disagrees?
1130 // MovePage creates a dummy revision with changed = false!
1131 // We may want to explicitly distinguish between "no new revision" (null-edit)
1132 // and "new revision without new content" (dummy revision).
1133
1134 if ( $oldId === $revision->getParentId() ) {
1135 // NOTE: this may still be a NullRevision!
1136 // New revision!
1137 $this->options['changed'] = true;
1138 } elseif ( $oldId === $revision->getId() ) {
1139 // Null-edit!
1140 $this->options['changed'] = false;
1141 } else {
1142 // This indicates that calling code has given us the wrong Revision object
1143 throw new LogicException(
1144 'The Revision mismatches old revision ID: '
1145 . 'Old ID is ' . $oldId
1146 . ', parent ID is ' . $revision->getParentId()
1147 . ', revision ID is ' . $revision->getId()
1148 );
1149 }
1150 }
1151
1152 // If prepareContent() was used to generate the PST content (which is indicated by
1153 // $this->slotsUpdate being set), and this is not a null-edit, then the given
1154 // revision must have the acting user as the revision author. Otherwise, user
1155 // signatures generated by PST would mismatch the user in the revision record.
1156 if ( $this->user !== null && $this->options['changed'] && $this->slotsUpdate ) {
1157 $user = $revision->getUser();
1158 if ( !$this->user->equals( $user ) ) {
1159 throw new LogicException(
1160 'The Revision provided has a mismatching actor: expected '
1161 . $this->user->getName()
1162 . ', got '
1163 . $user->getName()
1164 );
1165 }
1166 }
1167
1168 // If $this->pageState was not yet initialized by grabCurrentRevision or prepareContent,
1169 // emulate the state of the page table before the edit, as good as we can.
1170 if ( !$this->pageState ) {
1171 $this->pageState = [
1172 'oldIsRedirect' => isset( $this->options['oldredirect'] )
1173 && is_bool( $this->options['oldredirect'] )
1174 ? $this->options['oldredirect']
1175 : null,
1176 'oldCountable' => isset( $this->options['oldcountable'] )
1177 && is_bool( $this->options['oldcountable'] )
1178 ? $this->options['oldcountable']
1179 : null,
1180 ];
1181
1182 if ( $this->options['changed'] ) {
1183 // The edit created a new revision
1184 $this->pageState['oldId'] = $revision->getParentId();
1185
1186 if ( isset( $this->options['oldrevision'] ) ) {
1187 $rev = $this->options['oldrevision'];
1188 $this->pageState['oldRevision'] = $rev instanceof Revision
1190 : $rev;
1191 }
1192 } else {
1193 // This is a null-edit, so the old revision IS the new revision!
1194 $this->pageState['oldId'] = $revision->getId();
1195 $this->pageState['oldRevision'] = $revision;
1196 }
1197 }
1198
1199 // "created" is forced here
1200 $this->options['created'] = ( $this->options['created'] ||
1201 ( $this->pageState['oldId'] === 0 ) );
1202
1203 $this->revision = $revision;
1204
1205 $this->doTransition( 'has-revision' );
1206
1207 // NOTE: in case we have a User object, don't override with a UserIdentity.
1208 // We already checked that $revision->getUser() mathces $this->user;
1209 if ( !$this->user ) {
1210 $this->user = $revision->getUser( RevisionRecord::RAW );
1211 }
1212
1213 // Prune any output that depends on the revision ID.
1214 if ( $this->renderedRevision ) {
1215 $this->renderedRevision->updateRevision( $revision );
1216 } else {
1217
1218 // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
1219 // NOTE: the revision is either new or current, so we can bypass audience checks.
1220 $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
1221 $this->revision,
1222 null,
1223 null,
1224 [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ]
1225 );
1226
1227 // XXX: Since we presumably are dealing with the current revision,
1228 // we could try to get the ParserOutput from the parser cache.
1229 }
1230
1231 // TODO: optionally get ParserOutput from the ParserCache here.
1232 // Move the logic used by RefreshLinksJob here!
1233 }
1234
1239 public function getPreparedEdit() {
1240 $this->assertPrepared( __METHOD__ );
1241
1243 $preparedEdit = new PreparedEdit();
1244
1245 $preparedEdit->popts = $this->getCanonicalParserOptions();
1246 $preparedEdit->output = $this->getCanonicalParserOutput();
1247 $preparedEdit->pstContent = $this->revision->getContent( SlotRecord::MAIN );
1248 $preparedEdit->newContent =
1249 $slotsUpdate->isModifiedSlot( SlotRecord::MAIN )
1250 ? $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent()
1251 : $this->revision->getContent( SlotRecord::MAIN ); // XXX: can we just remove this?
1252 $preparedEdit->oldContent = null; // unused. // XXX: could get this from the parent revision
1253 $preparedEdit->revid = $this->revision ? $this->revision->getId() : null;
1254 $preparedEdit->timestamp = $preparedEdit->output->getCacheTime();
1255 $preparedEdit->format = $preparedEdit->pstContent->getDefaultFormat();
1256
1257 return $preparedEdit;
1258 }
1259
1265 public function getSlotParserOutput( $role, $generateHtml = true ) {
1266 return $this->getRenderedRevision()->getSlotParserOutput(
1267 $role,
1268 [ 'generate-html' => $generateHtml ]
1269 );
1270 }
1271
1275 public function getCanonicalParserOutput() {
1276 return $this->getRenderedRevision()->getRevisionParserOutput();
1277 }
1278
1282 public function getCanonicalParserOptions() {
1283 return $this->getRenderedRevision()->getOptions();
1284 }
1285
1291 public function getSecondaryDataUpdates( $recursive = false ) {
1292 if ( $this->isContentDeleted() ) {
1293 // This shouldn't happen, since the current content is always public,
1294 // and DataUpates are only needed for current content.
1295 return [];
1296 }
1297
1299
1300 // Construct a LinksUpdate for the combined canonical output.
1302 $this->getTitle(),
1303 $output,
1304 $recursive
1305 );
1306
1307 $allUpdates = [ $linksUpdate ];
1308
1309 // NOTE: Run updates for all slots, not just the modified slots! Otherwise,
1310 // info for an inherited slot may end up being removed. This is also needed
1311 // to ensure that purges are effective.
1313 foreach ( $this->getSlots()->getSlotRoles() as $role ) {
1314 $slot = $this->getRawSlot( $role );
1315 $content = $slot->getContent();
1316 $handler = $content->getContentHandler();
1317
1318 $updates = $handler->getSecondaryDataUpdates(
1319 $this->getTitle(),
1320 $content,
1321 $role,
1323 );
1324 $allUpdates = array_merge( $allUpdates, $updates );
1325
1326 // TODO: remove B/C hack in 1.32!
1327 // NOTE: we assume that the combined output contains all relevant meta-data for
1328 // all slots!
1329 $legacyUpdates = $content->getSecondaryDataUpdates(
1330 $this->getTitle(),
1331 null,
1332 $recursive,
1333 $output
1334 );
1335
1336 // HACK: filter out redundant and incomplete LinksUpdates
1337 $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1338 return !( $update instanceof LinksUpdate );
1339 } );
1340
1341 $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1342 }
1343
1344 // XXX: if a slot was removed by an earlier edit, but deletion updates failed to run at
1345 // that time, we don't know for which slots to run deletion updates when purging a page.
1346 // We'd have to examine the entire history of the page to determine that. Perhaps there
1347 // could be a "try extra hard" mode for that case that would run a DB query to find all
1348 // roles/models ever used on the page. On the other hand, removing slots should be quite
1349 // rare, so perhaps this isn't worth the trouble.
1350
1351 // TODO: consolidate with similar logic in WikiPage::getDeletionUpdates()
1352 $wikiPage = $this->getWikiPage();
1354 foreach ( $this->getRemovedSlotRoles() as $role ) {
1355 // HACK: we should get the content model of the removed slot from a SlotRoleHandler!
1356 // For now, find the slot in the parent revision - if the slot was removed, it should
1357 // always exist in the parent revision.
1358 $parentSlot = $parentRevision->getSlot( $role, RevisionRecord::RAW );
1359 $content = $parentSlot->getContent();
1360 $handler = $content->getContentHandler();
1361
1362 $updates = $handler->getDeletionUpdates(
1363 $this->getTitle(),
1364 $role
1365 );
1366 $allUpdates = array_merge( $allUpdates, $updates );
1367
1368 // TODO: remove B/C hack in 1.32!
1369 $legacyUpdates = $content->getDeletionUpdates( $wikiPage );
1370
1371 // HACK: filter out redundant and incomplete LinksDeletionUpdate
1372 $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1373 return !( $update instanceof LinksDeletionUpdate );
1374 } );
1375
1376 $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1377 }
1378
1379 // TODO: hard deprecate SecondaryDataUpdates in favor of RevisionDataUpdates in 1.33!
1380 Hooks::run(
1381 'RevisionDataUpdates',
1382 [ $this->getTitle(), $renderedRevision, &$allUpdates ]
1383 );
1384
1385 return $allUpdates;
1386 }
1387
1398 public function doUpdates() {
1399 $this->assertTransition( 'done' );
1400
1401 // TODO: move logic into a PageEventEmitter service
1402
1403 $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
1404
1405 $legacyUser = User::newFromIdentity( $this->user );
1406 $legacyRevision = new Revision( $this->revision );
1407
1408 $this->doParserCacheUpdate();
1409
1410 $this->doSecondaryDataUpdates( [
1411 // T52785 do not update any other pages on a null edit
1412 'recursive' => $this->options['changed'],
1413 'defer' => DeferredUpdates::POSTSEND,
1414 ] );
1415
1416 // TODO: MCR: check if *any* changed slot supports categories!
1417 if ( $this->rcWatchCategoryMembership
1418 && $this->getContentHandler( SlotRecord::MAIN )->supportsCategories() === true
1419 && ( $this->options['changed'] || $this->options['created'] )
1420 && !$this->options['restored']
1421 ) {
1422 // Note: jobs are pushed after deferred updates, so the job should be able to see
1423 // the recent change entry (also done via deferred updates) and carry over any
1424 // bot/deletion/IP flags, ect.
1425 $this->jobQueueGroup->lazyPush(
1427 $this->getTitle(),
1428 $this->revision->getTimestamp()
1429 )
1430 );
1431 }
1432
1433 // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1434 $editInfo = $this->getPreparedEdit();
1435 Hooks::run( 'ArticleEditUpdates', [ &$wikiPage, &$editInfo, $this->options['changed'] ] );
1436
1437 // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1438 if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
1439 // Flush old entries from the `recentchanges` table
1440 if ( mt_rand( 0, 9 ) == 0 ) {
1441 $this->jobQueueGroup->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
1442 }
1443 }
1444
1445 $id = $this->getPageId();
1446 $title = $this->getTitle();
1447 $dbKey = $title->getPrefixedDBkey();
1448 $shortTitle = $title->getDBkey();
1449
1450 if ( !$title->exists() ) {
1451 wfDebug( __METHOD__ . ": Page doesn't exist any more, bailing out\n" );
1452
1453 $this->doTransition( 'done' );
1454 return;
1455 }
1456
1457 if ( $this->options['oldcountable'] === 'no-change' ||
1458 ( !$this->options['changed'] && !$this->options['moved'] )
1459 ) {
1460 $good = 0;
1461 } elseif ( $this->options['created'] ) {
1462 $good = (int)$this->isCountable();
1463 } elseif ( $this->options['oldcountable'] !== null ) {
1464 $good = (int)$this->isCountable()
1465 - (int)$this->options['oldcountable'];
1466 } elseif ( isset( $this->pageState['oldCountable'] ) ) {
1467 $good = (int)$this->isCountable()
1468 - (int)$this->pageState['oldCountable'];
1469 } else {
1470 $good = 0;
1471 }
1472 $edits = $this->options['changed'] ? 1 : 0;
1473 $pages = $this->options['created'] ? 1 : 0;
1474
1475 DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
1476 [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
1477 ) );
1478
1479 // TODO: make search infrastructure aware of slots!
1480 $mainSlot = $this->revision->getSlot( SlotRecord::MAIN );
1481 if ( !$mainSlot->isInherited() && !$this->isContentDeleted() ) {
1482 DeferredUpdates::addUpdate( new SearchUpdate( $id, $dbKey, $mainSlot->getContent() ) );
1483 }
1484
1485 // If this is another user's talk page, update newtalk.
1486 // Don't do this if $options['changed'] = false (null-edits) nor if
1487 // it's a minor edit and the user making the edit doesn't generate notifications for those.
1488 if ( $this->options['changed']
1489 && $title->getNamespace() == NS_USER_TALK
1490 && $shortTitle != $legacyUser->getTitleKey()
1491 && !( $this->revision->isMinor() && $legacyUser->isAllowed( 'nominornewtalk' ) )
1492 ) {
1493 $recipient = User::newFromName( $shortTitle, false );
1494 if ( !$recipient ) {
1495 wfDebug( __METHOD__ . ": invalid username\n" );
1496 } else {
1497 // Allow extensions to prevent user notification
1498 // when a new message is added to their talk page
1499 // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1500 if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
1501 if ( User::isIP( $shortTitle ) ) {
1502 // An anonymous user
1503 $recipient->setNewtalk( true, $legacyRevision );
1504 } elseif ( $recipient->isLoggedIn() ) {
1505 $recipient->setNewtalk( true, $legacyRevision );
1506 } else {
1507 wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
1508 }
1509 }
1510 }
1511 }
1512
1513 if ( $title->getNamespace() == NS_MEDIAWIKI
1514 && $this->getRevisionSlotsUpdate()->isModifiedSlot( SlotRecord::MAIN )
1515 ) {
1516 $mainContent = $this->isContentDeleted() ? null : $this->getRawContent( SlotRecord::MAIN );
1517
1518 $this->messageCache->updateMessageOverride( $title, $mainContent );
1519 }
1520
1521 // TODO: move onArticleCreate and onArticle into a PageEventEmitter service
1522 if ( $this->options['created'] ) {
1523 WikiPage::onArticleCreate( $title );
1524 } elseif ( $this->options['changed'] ) { // T52785
1525 WikiPage::onArticleEdit( $title, $legacyRevision, $this->getTouchedSlotRoles() );
1526 }
1527
1528 $oldRevision = $this->getParentRevision();
1529 $oldLegacyRevision = $oldRevision ? new Revision( $oldRevision ) : null;
1530
1531 // TODO: In the wiring, register a listener for this on the new PageEventEmitter
1533 $title, $oldLegacyRevision, $legacyRevision, $this->getWikiId() ?: wfWikiID()
1534 );
1535
1536 $this->doTransition( 'done' );
1537 }
1538
1553 public function doSecondaryDataUpdates( array $options = [] ) {
1554 $this->assertHasRevision( __METHOD__ );
1555 $options += [
1556 'recursive' => false,
1557 'defer' => false,
1558 'transactionTicket' => null,
1559 ];
1560 $deferValues = [ false, DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND ];
1561 if ( !in_array( $options['defer'], $deferValues, true ) ) {
1562 throw new InvalidArgumentException( 'invalid value for defer: ' . $options['defer'] );
1563 }
1564 Assert::parameterType( 'integer|null', $options['transactionTicket'],
1565 '$options[\'transactionTicket\']' );
1566
1567 $updates = $this->getSecondaryDataUpdates( $options['recursive'] );
1568
1569 $triggeringUser = $this->options['triggeringUser'] ?? $this->user;
1570 if ( !$triggeringUser instanceof User ) {
1571 $triggeringUser = User::newFromIdentity( $triggeringUser );
1572 }
1573 $causeAction = $this->options['causeAction'] ?? 'unknown';
1574 $causeAgent = $this->options['causeAgent'] ?? 'unknown';
1575 $legacyRevision = new Revision( $this->revision );
1576
1577 if ( $options['defer'] === false && $options['transactionTicket'] !== null ) {
1578 // For legacy hook handlers doing updates via LinksUpdateConstructed, make sure
1579 // any pending writes they made get flushed before the doUpdate() calls below.
1580 // This avoids snapshot-clearing errors in LinksUpdate::acquirePageLock().
1581 $this->loadbalancerFactory->commitAndWaitForReplication(
1582 __METHOD__, $options['transactionTicket']
1583 );
1584 }
1585
1586 foreach ( $updates as $update ) {
1587 if ( $update instanceof DataUpdate ) {
1588 $update->setCause( $causeAction, $causeAgent );
1589 }
1590 if ( $update instanceof LinksUpdate ) {
1591 $update->setRevision( $legacyRevision );
1592 $update->setTriggeringUser( $triggeringUser );
1593 }
1594
1595 if ( $options['defer'] === false ) {
1596 if ( $update instanceof DataUpdate && $options['transactionTicket'] !== null ) {
1597 $update->setTransactionTicket( $options['transactionTicket'] );
1598 }
1599 $update->doUpdate();
1600 } else {
1601 DeferredUpdates::addUpdate( $update, $options['defer'] );
1602 }
1603 }
1604 }
1605
1606 public function doParserCacheUpdate() {
1607 $this->assertHasRevision( __METHOD__ );
1608
1609 $wikiPage = $this->getWikiPage(); // TODO: ParserCache should accept a RevisionRecord instead
1610
1611 // NOTE: this may trigger the first parsing of the new content after an edit (when not
1612 // using pre-generated stashed output).
1613 // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
1614 // to be performed post-send. The client could already follow a HTTP redirect to the
1615 // page view, but would then have to wait for a response until rendering is complete.
1617
1618 // Save it to the parser cache. Use the revision timestamp in the case of a
1619 // freshly saved edit, as that matches page_touched and a mismatch would trigger an
1620 // unnecessary reparse.
1621 $timestamp = $this->options['newrev'] ? $this->revision->getTimestamp()
1622 : $output->getCacheTime();
1623 $this->parserCache->save(
1625 $timestamp, $this->revision->getId()
1626 );
1627 }
1628
1629}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
Prepare an edit in shared cache so that it can be reused on edit.
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 newSpec(Title $title, $revisionTimestamp)
A content handler knows how do deal with a specific type of content on a wiki page.
Abstract base class for update jobs that do something with some secondary data extracted from article...
Class for managing the deferred updates.
Hooks class.
Definition Hooks.php:34
Class to handle enqueueing of background jobs.
Internationalisation code.
Definition Language.php:36
Update object handling the cleanup of links tables after a page was deleted.
Class the manages updates of *_link tables as well as similar extension-managed tables.
Represents information returned by WikiPage::prepareContentForEdit()
Mutable RevisionRecord implementation, for building new revision entries programmatically.
RenderedRevision represents the rendered representation of a revision.
Page revision base class.
getParentId()
Get parent revision ID (the original previous page revision).
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it's available to the specified audience.
getVisibility()
Get the deletion bitfield of the revision.
getSlots()
Returns the slots defined for this revision.
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
isMinor()
MCR migration note: this replaces Revision::isMinor.
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision's author's user identity, if it's available to the specified audience.
The RevisionRenderer service provides access to rendered output for revisions.
Value object representing the set of slots belonging to a revision.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
A handle for managing updates for derived page data on edit, import, purge, etc.
getModifiedSlotRoles()
Returns the role names of the slots modified by the new revision, not including removed roles.
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
static array[] $transitions
Transition table for managing the life cycle of DerivedPageDateUpdater instances.
string $stage
A stage identifier for managing the life cycle of this instance.
getParentRevision()
Returns the parent revision of the new revision wrapped by this update.
array $pageState
The state of the relevant row in page table before the edit.
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
isContentDeleted()
Whether the content is deleted and thus not visible to the public.
prepareUpdate(RevisionRecord $revision, array $options=[])
Prepare derived data updates targeting the given Revision.
string $articleCountMethod
see $wgArticleCountMethod
doTransition( $newStage)
Transition function for managing the life cycle of this instances.
isCreation()
Whether the edit creates the page.
getRemovedSlotRoles()
Returns the role names of the slots removed by the new revision.
getRawContent( $role)
Returns the content of the given slot, with no audience checks.
getRevisionSlotsUpdate()
Returns the RevisionSlotsUpdate for this updater.
isChange()
Whether the edit created, or should create, a new revision (that is, it's not a null-edit).
grabCurrentRevision()
Returns the revision that was the page's current revision when grabCurrentRevision() was first called...
doUpdates()
Do standard updates after page edit, purge, or import.
pageExisted()
Determines whether the page being edited already existed.
wasRedirect()
Whether the page was a redirect before the edit.
isUpdatePrepared()
Whether prepareUpdate() has been called on this instance.
getContentModel( $role)
Returns the content model of the given slot.
boolean $rcWatchCategoryMembership
see $wgRCWatchCategoryMembership
getSlots()
Returns the slots of the target revision, after PST.
__construct(WikiPage $wikiPage, RevisionStore $revisionStore, RevisionRenderer $revisionRenderer, SlotRoleRegistry $slotRoleRegistry, ParserCache $parserCache, JobQueueGroup $jobQueueGroup, MessageCache $messageCache, Language $contLang, LBFactory $loadbalancerFactory)
isReusableFor(UserIdentity $user=null, RevisionRecord $revision=null, RevisionSlotsUpdate $slotsUpdate=null, $parentId=null)
Checks whether this DerivedPageDataUpdater can be re-used for running updates targeting the given rev...
getTouchedSlotRoles()
Returns the role names of the slots touched by the new revision, including removed roles.
isContentPrepared()
Whether prepareUpdate() or prepareContent() have been called on this instance.
getRawSlot( $role)
Returns the slot, modified or inherited, after PST, with no audience checks applied.
$options
Stores (most of) the $options parameter of prepareUpdate().
assertTransition( $newStage)
Asserts that a transition to the given stage is possible, without performing it.
getRevision()
Returns the update's target revision - that is, the revision that will be the current revision after ...
prepareContent(User $user, RevisionSlotsUpdate $slotsUpdate, $useStash=true)
Prepare updates based on an update which has not yet been saved.
Value object representing a modification of revision slots.
getRemovedRoles()
Returns a list of removed slot roles, that is, roles removed by calling removeSlot(),...
hasSameUpdates(RevisionSlotsUpdate $other)
Returns true if $other represents the same update - that is, if all methods defined by RevisionSlotsU...
getModifiedRoles()
Returns a list of modified slot roles, that is, roles modified by calling modifySlot(),...
getModifiedSlot( $role)
Returns the SlotRecord associated with the given role, if the slot with that role was modified (and n...
static newFromRevisionSlots(RevisionSlots $newSlots, RevisionSlots $parentSlots=null)
Constructs a RevisionSlotsUpdate representing the update that turned $parentSlots into $newSlots.
isModifiedSlot( $role)
Returns whether getModifiedSlot() will return a SlotRecord for the given role.
Cache of messages that are defined by MediaWiki namespace pages or by hooks.
Set options of the Parser.
Job for pruning recent changes.
Abstraction for ResourceLoader modules which pull from wiki pages.
static invalidateModuleCache(Title $title, Revision $old=null, Revision $new=null, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
getRevisionRecord()
Definition Revision.php:629
Database independant search index updater.
Class for handling updates to the site_stats table.
static factory(array $deltas)
Represents a title within MediaWiki.
Definition Title.php:40
equals(Title $title)
Compare with another title.
Definition Title.php:4006
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:585
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition User.php:652
static isIP( $name)
Does the string match an anonymous IP address?
Definition User.php:967
Class representing a MediaWiki article and history.
Definition WikiPage.php:45
getRevision()
Get the latest revision.
Definition WikiPage.php:783
isRedirect()
Tests if the article content represents a redirect.
Definition WikiPage.php:630
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition WikiPage.php:485
isCountable( $editInfo=false)
Determine whether a page would be suitable for being counted as an article in the site_stats table ba...
Definition WikiPage.php:942
An interface for generating database load balancers.
Definition LBFactory.php:39
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
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 and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same user
Wikitext formatted, in the key only.
const NS_MEDIAWIKI
Definition Defines.php:81
const NS_USER_TALK
Definition Defines.php:76
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition hooks.txt:1834
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:894
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing options(say) and put it in one place. Instead of having little title-reversing if-blocks spread all over the codebase in showAnArticle
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition hooks.txt:783
either a unescaped string or a HtmlArmor object after in associative array form externallinks $linksUpdate
Definition hooks.txt:2088
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2272
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1779
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback 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 that deferrable updates should implement.
Interface for database access objects.
Interface for objects representing user identity.
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
In both all secondary updates will be triggered handle like object that caches derived data representing a revision
$content