MediaWiki REL1_32
PageUpdater.php
Go to the documentation of this file.
1<?php
25namespace MediaWiki\Storage;
26
34use InvalidArgumentException;
35use LogicException;
46use RuntimeException;
49use User;
50use Wikimedia\Assert\Assert;
56
73
77 private $user;
78
82 private $wikiPage;
83
88
93
98
104
108 private $rcPatrolStatus = RecentChange::PRC_UNPATROLLED;
109
113 private $usePageCreationLog = true;
114
118 private $ajaxEditStash = true;
119
123 private $originalRevId = false;
124
128 private $tags = [];
129
133 private $undidRevId = 0;
134
139
143 private $status = null;
144
152 public function __construct(
153 User $user,
158 ) {
159 $this->user = $user;
160 $this->wikiPage = $wikiPage;
161 $this->derivedDataUpdater = $derivedDataUpdater;
162
163 $this->loadBalancer = $loadBalancer;
164 $this->revisionStore = $revisionStore;
165
166 $this->slotsUpdate = new RevisionSlotsUpdate();
167 }
168
177 $this->useAutomaticEditSummaries = $useAutomaticEditSummaries;
178 }
179
189 public function setRcPatrolStatus( $status ) {
190 $this->rcPatrolStatus = $status;
191 }
192
200 public function setUsePageCreationLog( $use ) {
201 $this->usePageCreationLog = $use;
202 }
203
208 public function setAjaxEditStash( $ajaxEditStash ) {
209 $this->ajaxEditStash = $ajaxEditStash;
210 }
211
212 private function getWikiId() {
213 return false; // TODO: get from RevisionStore!
214 }
215
221 private function getDBConnectionRef( $mode ) {
222 return $this->loadBalancer->getConnectionRef( $mode, [], $this->getWikiId() );
223 }
224
228 private function getLinkTarget() {
229 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
230 return $this->wikiPage->getTitle();
231 }
232
236 private function getTitle() {
237 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
238 return $this->wikiPage->getTitle();
239 }
240
244 private function getWikiPage() {
245 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
246 return $this->wikiPage;
247 }
248
282 public function hasEditConflict( $expectedParentRevision ) {
283 $parent = $this->grabParentRevision();
284 $parentId = $parent ? $parent->getId() : 0;
285
286 return $parentId !== $expectedParentRevision;
287 }
288
316 public function grabParentRevision() {
317 return $this->derivedDataUpdater->grabCurrentRevision();
318 }
319
323 private function getTimestampNow() {
324 // TODO: allow an override to be injected for testing
325 return wfTimestampNow();
326 }
327
334 private function checkFlags( $flags ) {
335 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
336 $flags |= ( $this->derivedDataUpdater->pageExisted() ) ? EDIT_UPDATE : EDIT_NEW;
337 }
338
339 return $flags;
340 }
341
348 public function setContent( $role, Content $content ) {
349 // TODO: MCR: check the role and the content's model against the list of supported
350 // roles, see T194046.
351
352 $this->slotsUpdate->modifyContent( $role, $content );
353 }
354
360 public function setSlot( SlotRecord $slot ) {
361 $this->slotsUpdate->modifySlot( $slot );
362 }
363
378 public function inheritSlot( SlotRecord $originalSlot ) {
379 // NOTE: this slot is inherited from some other revision, but it's
380 // a "modified" slot for the RevisionSlotsUpdate and DerivedPageDataUpdater,
381 // since it's not implicitly inherited from the parent revision.
382 $inheritedSlot = SlotRecord::newInherited( $originalSlot );
383 $this->slotsUpdate->modifySlot( $inheritedSlot );
384 }
385
395 public function removeSlot( $role ) {
396 if ( $role === SlotRecord::MAIN ) {
397 throw new InvalidArgumentException( 'Cannot remove the main slot!' );
398 }
399
400 $this->slotsUpdate->removeSlot( $role );
401 }
402
409 public function getOriginalRevisionId() {
411 }
412
425 Assert::parameterType( 'integer|boolean', $originalRevId, '$originalRevId' );
426 $this->originalRevId = $originalRevId;
427 }
428
435 public function getUndidRevisionId() {
436 return $this->undidRevId;
437 }
438
446 public function setUndidRevisionId( $undidRevId ) {
447 Assert::parameterType( 'integer', $undidRevId, '$undidRevId' );
448 $this->undidRevId = $undidRevId;
449 }
450
457 public function addTag( $tag ) {
458 Assert::parameterType( 'string', $tag, '$tag' );
459 $this->tags[] = trim( $tag );
460 }
461
468 public function addTags( array $tags ) {
469 Assert::parameterElementType( 'string', $tags, '$tags' );
470 foreach ( $tags as $tag ) {
471 $this->addTag( $tag );
472 }
473 }
474
480 public function getExplicitTags() {
481 return $this->tags;
482 }
483
488 private function computeEffectiveTags( $flags ) {
490
491 foreach ( $this->slotsUpdate->getModifiedRoles() as $role ) {
492 $old_content = $this->getParentContent( $role );
493
494 $handler = $this->getContentHandler( $role );
495 $content = $this->slotsUpdate->getModifiedSlot( $role )->getContent();
496
497 // TODO: MCR: Do this for all slots. Also add tags for removing roles!
498 $tag = $handler->getChangeTag( $old_content, $content, $flags );
499 // If there is no applicable tag, null is returned, so we need to check
500 if ( $tag ) {
501 $tags[] = $tag;
502 }
503 }
504
505 // Check for undo tag
506 if ( $this->undidRevId !== 0 && in_array( 'mw-undo', ChangeTags::getSoftwareTags() ) ) {
507 $tags[] = 'mw-undo';
508 }
509
510 return array_unique( $tags );
511 }
512
520 private function getParentContent( $role ) {
521 $parent = $this->grabParentRevision();
522
523 if ( $parent && $parent->hasSlot( $role ) ) {
524 return $parent->getContent( $role, RevisionRecord::RAW );
525 }
526
527 return null;
528 }
529
534 private function getContentHandler( $role ) {
535 // TODO: inject something like a ContentHandlerRegistry
536 if ( $this->slotsUpdate->isModifiedSlot( $role ) ) {
537 $slot = $this->slotsUpdate->getModifiedSlot( $role );
538 } else {
539 $parent = $this->grabParentRevision();
540
541 if ( $parent ) {
542 $slot = $parent->getSlot( $role, RevisionRecord::RAW );
543 } else {
544 throw new RevisionAccessException( 'No such slot: ' . $role );
545 }
546 }
547
548 return ContentHandler::getForModelID( $slot->getModel() );
549 }
550
556 private function makeAutoSummary( $flags ) {
557 if ( !$this->useAutomaticEditSummaries || ( $flags & EDIT_AUTOSUMMARY ) === 0 ) {
558 return CommentStoreComment::newUnsavedComment( '' );
559 }
560
561 // NOTE: this generates an auto-summary for SOME RANDOM changed slot!
562 // TODO: combine auto-summaries for multiple slots!
563 // XXX: this logic should not be in the storage layer!
564 $roles = $this->slotsUpdate->getModifiedRoles();
565 $role = reset( $roles );
566
567 if ( $role === false ) {
568 return CommentStoreComment::newUnsavedComment( '' );
569 }
570
571 $handler = $this->getContentHandler( $role );
572 $content = $this->slotsUpdate->getModifiedSlot( $role )->getContent();
573 $old_content = $this->getParentContent( $role );
574 $summary = $handler->getAutosummary( $old_content, $content, $flags );
575
576 return CommentStoreComment::newUnsavedComment( $summary );
577 }
578
624 public function saveRevision( CommentStoreComment $summary, $flags = 0 ) {
625 // Defend against mistakes caused by differences with the
626 // signature of WikiPage::doEditContent.
627 Assert::parameterType( 'integer', $flags, '$flags' );
628
629 if ( $this->wasCommitted() ) {
630 throw new RuntimeException( 'saveRevision() has already been called on this PageUpdater!' );
631 }
632
633 // Low-level sanity check
634 if ( $this->getLinkTarget()->getText() === '' ) {
635 throw new RuntimeException( 'Something is trying to edit an article with an empty title' );
636 }
637
638 // TODO: MCR: check the role and the content's model against the list of supported
639 // and required roles, see T194046.
640
641 // Make sure the given content type is allowed for this page
642 // TODO: decide: Extend check to other slots? Consider the role in check? [PageType]
643 $mainContentHandler = $this->getContentHandler( SlotRecord::MAIN );
644 if ( !$mainContentHandler->canBeUsedOn( $this->getTitle() ) ) {
645 $this->status = Status::newFatal( 'content-not-allowed-here',
646 ContentHandler::getLocalizedName( $mainContentHandler->getModelID() ),
647 $this->getTitle()->getPrefixedText()
648 );
649 return null;
650 }
651
652 // Load the data from the master database if needed. Needed to check flags.
653 // NOTE: This grabs the parent revision as the CAS token, if grabParentRevision
654 // wasn't called yet. If the page is modified by another process before we are done with
655 // it, this method must fail (with status 'edit-conflict')!
656 // NOTE: The parent revision may be different from $this->originalRevisionId.
657 $this->grabParentRevision();
658 $flags = $this->checkFlags( $flags );
659
660 // Avoid statsd noise and wasted cycles check the edit stash (T136678)
661 if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
662 $useStashed = false;
663 } else {
664 $useStashed = $this->ajaxEditStash;
665 }
666
667 // TODO: use this only for the legacy hook, and only if something uses the legacy hook
668 $wikiPage = $this->getWikiPage();
669
671
672 // Prepare the update. This performs PST and generates the canonical ParserOutput.
673 $this->derivedDataUpdater->prepareContent(
674 $this->user,
675 $this->slotsUpdate,
676 $useStashed
677 );
678
679 // TODO: don't force initialization here!
680 // This is a hack to work around the fact that late initialization of the ParserOutput
681 // causes ApiFlowEditHeaderTest::testCache to fail. Whether that failure indicates an
682 // actual problem, or is just an issue with the test setup, remains to be determined
683 // [dk, 2018-03].
684 // Anomie said in 2018-03:
685 /*
686 I suspect that what's breaking is this:
687
688 The old version of WikiPage::doEditContent() called prepareContentForEdit() which
689 generated the ParserOutput right then, so when doEditUpdates() gets called from the
690 DeferredUpdate scheduled by WikiPage::doCreate() there's no need to parse. I note
691 there's a comment there that says "Get the pre-save transform content and final
692 parser output".
693 The new version of WikiPage::doEditContent() makes a PageUpdater and calls its
694 saveRevision(), which calls DerivedPageDataUpdater::prepareContent() and
695 PageUpdater::doCreate() without ever having to actually generate a ParserOutput.
696 Thus, when DerivedPageDataUpdater::doUpdates() is called from the DeferredUpdate
697 scheduled by PageUpdater::doCreate(), it does find that it needs to parse at that point.
698
699 And the order of operations in that Flow test is presumably:
700
701 - Create a page with a call to WikiPage::doEditContent(), in a way that somehow avoids
702 processing the DeferredUpdate.
703 - Set up the "no set!" mock cache in Flow\Tests\Api\ApiTestCase::expectCacheInvalidate()
704 - Then, during the course of doing that test, a $db->commit() results in the
705 DeferredUpdates being run.
706 */
707 $this->derivedDataUpdater->getCanonicalParserOutput();
708
709 $mainContent = $this->derivedDataUpdater->getSlots()->getContent( SlotRecord::MAIN );
710
711 // Trigger pre-save hook (using provided edit summary)
712 $hookStatus = Status::newGood( [] );
713 // TODO: replace legacy hook!
714 // TODO: avoid pass-by-reference, see T193950
715 $hook_args = [ &$wikiPage, &$user, &$mainContent, &$summary,
716 $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
717 // Check if the hook rejected the attempted save
718 if ( !Hooks::run( 'PageContentSave', $hook_args ) ) {
719 if ( $hookStatus->isOK() ) {
720 // Hook returned false but didn't call fatal(); use generic message
721 $hookStatus->fatal( 'edit-hook-aborted' );
722 }
723
724 $this->status = $hookStatus;
725 return null;
726 }
727
728 // Provide autosummaries if one is not provided and autosummaries are enabled
729 // XXX: $summary == null seems logical, but the empty string may actually come from the user
730 // XXX: Move this logic out of the storage layer! It does not belong here! Use a callback?
731 if ( $summary->text === '' && $summary->data === null ) {
732 $summary = $this->makeAutoSummary( $flags );
733 }
734
735 // Actually create the revision and create/update the page.
736 // Do NOT yet set $this->status!
737 if ( $flags & EDIT_UPDATE ) {
738 $status = $this->doModify( $summary, $this->user, $flags );
739 } else {
740 $status = $this->doCreate( $summary, $this->user, $flags );
741 }
742
743 // Promote user to any groups they meet the criteria for
744 DeferredUpdates::addCallableUpdate( function () use ( $user ) {
745 $user->addAutopromoteOnceGroups( 'onEdit' );
746 $user->addAutopromoteOnceGroups( 'onView' ); // b/c
747 } );
748
749 // NOTE: set $this->status only after all hooks have been called,
750 // so wasCommitted doesn't return true wehn called indirectly from a hook handler!
751 $this->status = $status;
752
753 // TODO: replace bad status with Exceptions!
754 return ( $this->status && $this->status->isOK() )
755 ? $this->status->value['revision-record']
756 : null;
757 }
758
764 public function wasCommitted() {
765 return $this->status !== null;
766 }
767
791 public function getStatus() {
792 return $this->status;
793 }
794
800 public function wasSuccessful() {
801 return $this->status && $this->status->isOK();
802 }
803
809 public function isNew() {
810 return $this->status && $this->status->isOK() && $this->status->value['new'];
811 }
812
820 public function isUnchanged() {
821 return $this->status
822 && $this->status->isOK()
823 && $this->status->value['revision-record'] === null;
824 }
825
832 public function getNewRevision() {
833 return ( $this->status && $this->status->isOK() )
834 ? $this->status->value['revision-record']
835 : null;
836 }
837
853 private function makeNewRevision(
854 CommentStoreComment $comment,
855 User $user,
856 $flags,
858 ) {
859 $wikiPage = $this->getWikiPage();
860 $title = $this->getTitle();
861 $parent = $this->grabParentRevision();
862
863 // XXX: we expect to get a MutableRevisionRecord here, but that's a bit brittle!
864 // TODO: introduce something like an UnsavedRevisionFactory service instead!
866 $rev = $this->derivedDataUpdater->getRevision();
867
868 $rev->setPageId( $title->getArticleID() );
869
870 if ( $parent ) {
871 $oldid = $parent->getId();
872 $rev->setParentId( $oldid );
873 } else {
874 $oldid = 0;
875 }
876
877 $rev->setComment( $comment );
878 $rev->setUser( $user );
879 $rev->setMinorEdit( ( $flags & EDIT_MINOR ) > 0 );
880
881 foreach ( $rev->getSlots()->getSlots() as $slot ) {
882 $content = $slot->getContent();
883
884 // XXX: We may push this up to the "edit controller" level, see T192777.
885 // TODO: change the signature of PrepareSave to not take a WikiPage!
886 $prepStatus = $content->prepareSave( $wikiPage, $flags, $oldid, $user );
887
888 // TODO: MCR: record which problem arose in which slot.
889 $status->merge( $prepStatus );
890 }
891
892 return $rev;
893 }
894
903 private function doModify( CommentStoreComment $summary, User $user, $flags ) {
904 $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
905
906 // Update article, but only if changed.
907 $status = Status::newGood( [ 'new' => false, 'revision' => null, 'revision-record' => null ] );
908
909 $oldRev = $this->grabParentRevision();
910 $oldid = $oldRev ? $oldRev->getId() : 0;
911
912 if ( !$oldRev ) {
913 // Article gone missing
914 $status->fatal( 'edit-gone-missing' );
915
916 return $status;
917 }
918
919 $newRevisionRecord = $this->makeNewRevision(
920 $summary,
921 $user,
922 $flags,
923 $status
924 );
925
926 if ( !$status->isOK() ) {
927 return $status;
928 }
929
930 $now = $newRevisionRecord->getTimestamp();
931
932 // XXX: we may want a flag that allows a null revision to be forced!
933 $changed = $this->derivedDataUpdater->isChange();
934
935 $dbw = $this->getDBConnectionRef( DB_MASTER );
936
937 if ( $changed ) {
938 $dbw->startAtomic( __METHOD__ );
939
940 // Get the latest page_latest value while locking it.
941 // Do a CAS style check to see if it's the same as when this method
942 // started. If it changed then bail out before touching the DB.
943 $latestNow = $wikiPage->lockAndGetLatest(); // TODO: move to storage service, pass DB
944 if ( $latestNow != $oldid ) {
945 // We don't need to roll back, since we did not modify the database yet.
946 // XXX: Or do we want to rollback, any transaction started by calling
947 // code will fail? If we want that, we should probably throw an exception.
948 $dbw->endAtomic( __METHOD__ );
949 // Page updated or deleted in the mean time
950 $status->fatal( 'edit-conflict' );
951
952 return $status;
953 }
954
955 // At this point we are now comitted to returning an OK
956 // status unless some DB query error or other exception comes up.
957 // This way callers don't have to call rollback() if $status is bad
958 // unless they actually try to catch exceptions (which is rare).
959
960 // Save revision content and meta-data
961 $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
962 $newLegacyRevision = new Revision( $newRevisionRecord );
963
964 // Update page_latest and friends to reflect the new revision
965 // TODO: move to storage service
966 $wasRedirect = $this->derivedDataUpdater->wasRedirect();
967 if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, null, $wasRedirect ) ) {
968 throw new PageUpdateException( "Failed to update page row to use new revision." );
969 }
970
971 // TODO: replace legacy hook!
972 $tags = $this->computeEffectiveTags( $flags );
973 Hooks::run(
974 'NewRevisionFromEditComplete',
975 [ $wikiPage, $newLegacyRevision, $this->getOriginalRevisionId(), $user, &$tags ]
976 );
977
978 // Update recentchanges
979 if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
980 // Add RC row to the DB
981 RecentChange::notifyEdit(
982 $now,
983 $this->getTitle(),
984 $newRevisionRecord->isMinor(),
985 $user,
986 $summary->text, // TODO: pass object when that becomes possible
987 $oldid,
988 $newRevisionRecord->getTimestamp(),
989 ( $flags & EDIT_FORCE_BOT ) > 0,
990 '',
991 $oldRev->getSize(),
992 $newRevisionRecord->getSize(),
993 $newRevisionRecord->getId(),
994 $this->rcPatrolStatus,
995 $tags
996 );
997 }
998
1000
1001 $dbw->endAtomic( __METHOD__ );
1002
1003 // Return the new revision to the caller
1004 $status->value['revision-record'] = $newRevisionRecord;
1005
1006 // TODO: globally replace usages of 'revision' with getNewRevision()
1007 $status->value['revision'] = $newLegacyRevision;
1008 } else {
1009 // T34948: revision ID must be set to page {{REVISIONID}} and
1010 // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
1011 // Since we don't insert a new revision into the database, the least
1012 // error-prone way is to reuse given old revision.
1013 $newRevisionRecord = $oldRev;
1014
1015 $status->warning( 'edit-no-change' );
1016 // Update page_touched as updateRevisionOn() was not called.
1017 // Other cache updates are managed in WikiPage::onArticleEdit()
1018 // via WikiPage::doEditUpdates().
1019 $this->getTitle()->invalidateCache( $now );
1020 }
1021
1022 // Do secondary updates once the main changes have been committed...
1023 // NOTE: the updates have to be processed before sending the response to the client
1024 // (DeferredUpdates::PRESEND), otherwise the client may already be following the
1025 // HTTP redirect to the standard view before dervide data has been created - most
1026 // importantly, before the parser cache has been updated. This would cause the
1027 // content to be parsed a second time, or may cause stale content to be shown.
1028 DeferredUpdates::addUpdate(
1030 $dbw,
1031 $wikiPage,
1032 $newRevisionRecord,
1033 $user,
1034 $summary,
1035 $flags,
1036 $status,
1037 [ 'changed' => $changed, ]
1038 ),
1039 DeferredUpdates::PRESEND
1040 );
1041
1042 return $status;
1043 }
1044
1054 private function doCreate( CommentStoreComment $summary, User $user, $flags ) {
1055 $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
1056
1057 if ( !$this->derivedDataUpdater->getSlots()->hasSlot( SlotRecord::MAIN ) ) {
1058 throw new PageUpdateException( 'Must provide a main slot when creating a page!' );
1059 }
1060
1061 $status = Status::newGood( [ 'new' => true, 'revision' => null, 'revision-record' => null ] );
1062
1063 $newRevisionRecord = $this->makeNewRevision(
1064 $summary,
1065 $user,
1066 $flags,
1067 $status
1068 );
1069
1070 if ( !$status->isOK() ) {
1071 return $status;
1072 }
1073
1074 $now = $newRevisionRecord->getTimestamp();
1075
1076 $dbw = $this->getDBConnectionRef( DB_MASTER );
1077 $dbw->startAtomic( __METHOD__ );
1078
1079 // Add the page record unless one already exists for the title
1080 // TODO: move to storage service
1081 $newid = $wikiPage->insertOn( $dbw );
1082 if ( $newid === false ) {
1083 $dbw->endAtomic( __METHOD__ ); // nothing inserted
1084 $status->fatal( 'edit-already-exists' );
1085
1086 return $status; // nothing done
1087 }
1088
1089 // At this point we are now comitted to returning an OK
1090 // status unless some DB query error or other exception comes up.
1091 // This way callers don't have to call rollback() if $status is bad
1092 // unless they actually try to catch exceptions (which is rare).
1093 $newRevisionRecord->setPageId( $newid );
1094
1095 // Save the revision text...
1096 $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
1097 $newLegacyRevision = new Revision( $newRevisionRecord );
1098
1099 // Update the page record with revision data
1100 // TODO: move to storage service
1101 if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, 0 ) ) {
1102 throw new PageUpdateException( "Failed to update page row to use new revision." );
1103 }
1104
1105 // TODO: replace legacy hook!
1106 $tags = $this->computeEffectiveTags( $flags );
1107 Hooks::run(
1108 'NewRevisionFromEditComplete',
1109 [ $wikiPage, $newLegacyRevision, false, $user, &$tags ]
1110 );
1111
1112 // Update recentchanges
1113 if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1114 // Add RC row to the DB
1115 RecentChange::notifyNew(
1116 $now,
1117 $this->getTitle(),
1118 $newRevisionRecord->isMinor(),
1119 $user,
1120 $summary->text, // TODO: pass object when that becomes possible
1121 ( $flags & EDIT_FORCE_BOT ) > 0,
1122 '',
1123 $newRevisionRecord->getSize(),
1124 $newRevisionRecord->getId(),
1125 $this->rcPatrolStatus,
1126 $tags
1127 );
1128 }
1129
1131
1132 if ( $this->usePageCreationLog ) {
1133 // Log the page creation
1134 // @TODO: Do we want a 'recreate' action?
1135 $logEntry = new ManualLogEntry( 'create', 'create' );
1136 $logEntry->setPerformer( $user );
1137 $logEntry->setTarget( $this->getTitle() );
1138 $logEntry->setComment( $summary->text );
1139 $logEntry->setTimestamp( $now );
1140 $logEntry->setAssociatedRevId( $newRevisionRecord->getId() );
1141 $logEntry->insert();
1142 // Note that we don't publish page creation events to recentchanges
1143 // (i.e. $logEntry->publish()) since this would create duplicate entries,
1144 // one for the edit and one for the page creation.
1145 }
1146
1147 $dbw->endAtomic( __METHOD__ );
1148
1149 // Return the new revision to the caller
1150 // TODO: globally replace usages of 'revision' with getNewRevision()
1151 $status->value['revision'] = $newLegacyRevision;
1152 $status->value['revision-record'] = $newRevisionRecord;
1153
1154 // Do secondary updates once the main changes have been committed...
1155 DeferredUpdates::addUpdate(
1157 $dbw,
1158 $wikiPage,
1159 $newRevisionRecord,
1160 $user,
1161 $summary,
1162 $flags,
1163 $status,
1164 [ 'created' => true ]
1165 ),
1166 DeferredUpdates::PRESEND
1167 );
1168
1169 return $status;
1170 }
1171
1172 private function getAtomicSectionUpdate(
1173 IDatabase $dbw,
1175 RevisionRecord $newRevisionRecord,
1176 User $user,
1177 CommentStoreComment $summary,
1178 $flags,
1180 $hints = []
1181 ) {
1182 return new AtomicSectionUpdate(
1183 $dbw,
1184 __METHOD__,
1185 function () use (
1186 $wikiPage, $newRevisionRecord, $user,
1187 $summary, $flags, $status, $hints
1188 ) {
1189 // set debug data
1190 $hints['causeAction'] = 'edit-page';
1191 $hints['causeAgent'] = $user->getName();
1192
1193 $newLegacyRevision = new Revision( $newRevisionRecord );
1194 $mainContent = $newRevisionRecord->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
1195
1196 // Update links tables, site stats, etc.
1197 $this->derivedDataUpdater->prepareUpdate( $newRevisionRecord, $hints );
1198 $this->derivedDataUpdater->doUpdates();
1199
1200 // TODO: replace legacy hook!
1201 // TODO: avoid pass-by-reference, see T193950
1202
1203 if ( $hints['created'] ?? false ) {
1204 // Trigger post-create hook
1205 $params = [ &$wikiPage, &$user, $mainContent, $summary->text,
1206 $flags & EDIT_MINOR, null, null, &$flags, $newLegacyRevision ];
1207 Hooks::run( 'PageContentInsertComplete', $params );
1208 }
1209
1210 // Trigger post-save hook
1211 $params = [ &$wikiPage, &$user, $mainContent, $summary->text,
1212 $flags & EDIT_MINOR, null, null, &$flags, $newLegacyRevision,
1214 Hooks::run( 'PageContentSaveComplete', $params );
1215 }
1216 );
1217 }
1218
1219}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Deferrable Update for closure/callback updates via IDatabase::doAtomicSection()
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
CommentStoreComment represents a comment stored by CommentStore.
A content handler knows how do deal with a specific type of content on a wiki page.
Class for managing the deferred updates.
Hooks class.
Definition Hooks.php:34
MediaWiki exception.
Class for creating new log entries and inserting them into the database.
Definition LogEntry.php:437
Mutable RevisionRecord implementation, for building new revision entries programmatically.
Exception representing a failure to look up a revision.
Page revision base class.
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
A handle for managing updates for derived page data on edit, import, purge, etc.
Exception representing a failure to update a page entry.
Controller-like object for creating and updating pages by creating new revisions.
getStatus()
The Status object indicating whether saveRevision() was successful, or null if saveRevision() was not...
getUndidRevisionId()
Returns the revision ID set by setUndidRevisionId(), indicating what revision is being undone by this...
__construct(User $user, WikiPage $wikiPage, DerivedPageDataUpdater $derivedDataUpdater, LoadBalancer $loadBalancer, RevisionStore $revisionStore)
inheritSlot(SlotRecord $originalSlot)
Explicitly inherit a slot from some earlier revision.
wasSuccessful()
Whether saveRevision() completed successfully.
boolean $ajaxEditStash
see $wgAjaxEditStash
RevisionSlotsUpdate $slotsUpdate
saveRevision(CommentStoreComment $summary, $flags=0)
Change an existing article or create a new article.
isNew()
Whether saveRevision() was called and created a new page.
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
addTag( $tag)
Sets a tag to apply to this update.
isUnchanged()
Whether saveRevision() did not create a revision because the content didn't change (null-edit).
int $rcPatrolStatus
the RC patrol status the new revision should be marked with.
getParentContent( $role)
Returns the content of the given slot of the parent revision, with no audience checks applied.
getAtomicSectionUpdate(IDatabase $dbw, WikiPage $wikiPage, RevisionRecord $newRevisionRecord, User $user, CommentStoreComment $summary, $flags, Status $status, $hints=[])
makeNewRevision(CommentStoreComment $comment, User $user, $flags, Status $status)
Constructs a MutableRevisionRecord based on the Content prepared by the DerivedPageDataUpdater.
getNewRevision()
The new revision created by saveRevision(), or null if saveRevision() has not yet been called,...
setUsePageCreationLog( $use)
Whether to create a log entry for new page creations.
removeSlot( $role)
Removes the slot with the given role.
setOriginalRevisionId( $originalRevId)
Sets the ID of an earlier revision that is being repeated or restored by this update.
addTags(array $tags)
Sets tags to apply to this update.
setUseAutomaticEditSummaries( $useAutomaticEditSummaries)
Can be used to enable or disable automatic summaries that are applied to certain kinds of changes,...
grabParentRevision()
Returns the revision that was the page's current revision when grabParentRevision() was first called.
hasEditConflict( $expectedParentRevision)
Checks whether this update conflicts with another update performed between the client loading data to...
doCreate(CommentStoreComment $summary, User $user, $flags)
setRcPatrolStatus( $status)
Sets the "patrolled" status of the edit.
setUndidRevisionId( $undidRevId)
Sets the ID of revision that was undone by the present update.
wasCommitted()
Whether saveRevision() has been called on this instance.
setAjaxEditStash( $ajaxEditStash)
doModify(CommentStoreComment $summary, User $user, $flags)
DerivedPageDataUpdater $derivedDataUpdater
getExplicitTags()
Returns the list of tags set using the addTag() method.
boolean $useAutomaticEditSummaries
see $wgUseAutomaticEditSummaries
setSlot(SlotRecord $slot)
Set the new slot for the given slot role.
bool $usePageCreationLog
whether to create a log entry for new page creations.
setContent( $role, Content $content)
Set the new content for the given slot role.
getOriginalRevisionId()
Returns the ID of an earlier revision that is being repeated or restored by this update.
Value object representing a modification of revision slots.
Utility class for creating new RC entries.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:40
Represents a title within MediaWiki.
Definition Title.php:39
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:2462
addAutopromoteOnceGroups( $event)
Add the user to the group if he/she meets given criteria.
Definition User.php:1640
incEditCount()
Deferred version of incEditCountImmediate()
Definition User.php:5329
Class representing a MediaWiki article and history.
Definition WikiPage.php:44
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Helper class to handle automatically marking connections as reusable (via RAII pattern) as well handl...
Definition DBConnRef.php:15
Database connection, tracking, load balancing, and transaction manager for a cluster.
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 EDIT_FORCE_BOT
Definition Defines.php:156
const EDIT_INTERNAL
Definition Defines.php:159
const EDIT_UPDATE
Definition Defines.php:153
const EDIT_SUPPRESS_RC
Definition Defines.php:155
const EDIT_MINOR
Definition Defines.php:154
const EDIT_AUTOSUMMARY
Definition Defines.php:158
const EDIT_NEW
Definition Defines.php:152
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1305
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:994
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:933
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1818
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
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
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))
$parent
$content
const DB_MASTER
Definition defines.php:26
$params