MediaWiki REL1_33
PageUpdater.php
Go to the documentation of this file.
1<?php
25namespace MediaWiki\Storage;
26
34use LogicException;
46use RuntimeException;
49use User;
50use Wikimedia\Assert\Assert;
56
73
77 private $user;
78
82 private $wikiPage;
83
88
93
98
103
109
113 private $rcPatrolStatus = RecentChange::PRC_UNPATROLLED;
114
118 private $usePageCreationLog = true;
119
123 private $ajaxEditStash = true;
124
128 private $originalRevId = false;
129
133 private $tags = [];
134
138 private $undidRevId = 0;
139
144
148 private $status = null;
149
158 public function __construct(
159 User $user,
165 ) {
166 $this->user = $user;
167 $this->wikiPage = $wikiPage;
168 $this->derivedDataUpdater = $derivedDataUpdater;
169
170 $this->loadBalancer = $loadBalancer;
171 $this->revisionStore = $revisionStore;
172 $this->slotRoleRegistry = $slotRoleRegistry;
173
174 $this->slotsUpdate = new RevisionSlotsUpdate();
175 }
176
185 $this->useAutomaticEditSummaries = $useAutomaticEditSummaries;
186 }
187
197 public function setRcPatrolStatus( $status ) {
198 $this->rcPatrolStatus = $status;
199 }
200
208 public function setUsePageCreationLog( $use ) {
209 $this->usePageCreationLog = $use;
210 }
211
216 public function setAjaxEditStash( $ajaxEditStash ) {
217 $this->ajaxEditStash = $ajaxEditStash;
218 }
219
220 private function getWikiId() {
221 return false; // TODO: get from RevisionStore!
222 }
223
229 private function getDBConnectionRef( $mode ) {
230 return $this->loadBalancer->getConnectionRef( $mode, [], $this->getWikiId() );
231 }
232
236 private function getLinkTarget() {
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 getTitle() {
245 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
246 return $this->wikiPage->getTitle();
247 }
248
252 private function getWikiPage() {
253 // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
254 return $this->wikiPage;
255 }
256
290 public function hasEditConflict( $expectedParentRevision ) {
291 $parent = $this->grabParentRevision();
292 $parentId = $parent ? $parent->getId() : 0;
293
294 return $parentId !== $expectedParentRevision;
295 }
296
324 public function grabParentRevision() {
325 return $this->derivedDataUpdater->grabCurrentRevision();
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 $this->ensureRoleAllowed( $role );
350
351 $this->slotsUpdate->modifyContent( $role, $content );
352 }
353
359 public function setSlot( SlotRecord $slot ) {
360 $this->ensureRoleAllowed( $slot->getRole() );
361
362 $this->slotsUpdate->modifySlot( $slot );
363 }
364
379 public function inheritSlot( SlotRecord $originalSlot ) {
380 // NOTE: slots can be inherited even if the role is not "allowed" on the title.
381 // NOTE: this slot is inherited from some other revision, but it's
382 // a "modified" slot for the RevisionSlotsUpdate and DerivedPageDataUpdater,
383 // since it's not implicitly inherited from the parent revision.
384 $inheritedSlot = SlotRecord::newInherited( $originalSlot );
385 $this->slotsUpdate->modifySlot( $inheritedSlot );
386 }
387
397 public function removeSlot( $role ) {
398 $this->ensureRoleNotRequired( $role );
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 // NOTE: slots can be inherited even if the role is not "allowed" on the title.
639 $status = Status::newGood();
641 $this->slotsUpdate->getModifiedRoles(),
642 $status
643 );
645 $this->slotsUpdate->getRemovedRoles(),
646 $status
647 );
648
649 if ( !$status->isOK() ) {
650 return null;
651 }
652
653 // Make sure the given content is allowed in the respective slots of this page
654 foreach ( $this->slotsUpdate->getModifiedRoles() as $role ) {
655 $slot = $this->slotsUpdate->getModifiedSlot( $role );
656 $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
657
658 if ( !$roleHandler->isAllowedModel( $slot->getModel(), $this->getTitle() ) ) {
659 $contentHandler = ContentHandler::getForModelID( $slot->getModel() );
660 $this->status = Status::newFatal( 'content-not-allowed-here',
661 ContentHandler::getLocalizedName( $contentHandler->getModelID() ),
662 $this->getTitle()->getPrefixedText(),
663 wfMessage( $roleHandler->getNameMessageKey() )
664 // TODO: defer message lookup to caller
665 );
666 return null;
667 }
668 }
669
670 // Load the data from the master database if needed. Needed to check flags.
671 // NOTE: This grabs the parent revision as the CAS token, if grabParentRevision
672 // wasn't called yet. If the page is modified by another process before we are done with
673 // it, this method must fail (with status 'edit-conflict')!
674 // NOTE: The parent revision may be different from $this->originalRevisionId.
675 $this->grabParentRevision();
676 $flags = $this->checkFlags( $flags );
677
678 // Avoid statsd noise and wasted cycles check the edit stash (T136678)
679 if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
680 $useStashed = false;
681 } else {
682 $useStashed = $this->ajaxEditStash;
683 }
684
685 // TODO: use this only for the legacy hook, and only if something uses the legacy hook
686 $wikiPage = $this->getWikiPage();
687
689
690 // Prepare the update. This performs PST and generates the canonical ParserOutput.
691 $this->derivedDataUpdater->prepareContent(
692 $this->user,
693 $this->slotsUpdate,
694 $useStashed
695 );
696
697 // TODO: don't force initialization here!
698 // This is a hack to work around the fact that late initialization of the ParserOutput
699 // causes ApiFlowEditHeaderTest::testCache to fail. Whether that failure indicates an
700 // actual problem, or is just an issue with the test setup, remains to be determined
701 // [dk, 2018-03].
702 // Anomie said in 2018-03:
703 /*
704 I suspect that what's breaking is this:
705
706 The old version of WikiPage::doEditContent() called prepareContentForEdit() which
707 generated the ParserOutput right then, so when doEditUpdates() gets called from the
708 DeferredUpdate scheduled by WikiPage::doCreate() there's no need to parse. I note
709 there's a comment there that says "Get the pre-save transform content and final
710 parser output".
711 The new version of WikiPage::doEditContent() makes a PageUpdater and calls its
712 saveRevision(), which calls DerivedPageDataUpdater::prepareContent() and
713 PageUpdater::doCreate() without ever having to actually generate a ParserOutput.
714 Thus, when DerivedPageDataUpdater::doUpdates() is called from the DeferredUpdate
715 scheduled by PageUpdater::doCreate(), it does find that it needs to parse at that point.
716
717 And the order of operations in that Flow test is presumably:
718
719 - Create a page with a call to WikiPage::doEditContent(), in a way that somehow avoids
720 processing the DeferredUpdate.
721 - Set up the "no set!" mock cache in Flow\Tests\Api\ApiTestCase::expectCacheInvalidate()
722 - Then, during the course of doing that test, a $db->commit() results in the
723 DeferredUpdates being run.
724 */
725 $this->derivedDataUpdater->getCanonicalParserOutput();
726
727 $mainContent = $this->derivedDataUpdater->getSlots()->getContent( SlotRecord::MAIN );
728
729 // Trigger pre-save hook (using provided edit summary)
730 $hookStatus = Status::newGood( [] );
731 // TODO: replace legacy hook!
732 // TODO: avoid pass-by-reference, see T193950
733 $hook_args = [ &$wikiPage, &$user, &$mainContent, &$summary,
734 $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
735 // Check if the hook rejected the attempted save
736 if ( !Hooks::run( 'PageContentSave', $hook_args ) ) {
737 if ( $hookStatus->isOK() ) {
738 // Hook returned false but didn't call fatal(); use generic message
739 $hookStatus->fatal( 'edit-hook-aborted' );
740 }
741
742 $this->status = $hookStatus;
743 return null;
744 }
745
746 // Provide autosummaries if one is not provided and autosummaries are enabled
747 // XXX: $summary == null seems logical, but the empty string may actually come from the user
748 // XXX: Move this logic out of the storage layer! It does not belong here! Use a callback?
749 if ( $summary->text === '' && $summary->data === null ) {
750 $summary = $this->makeAutoSummary( $flags );
751 }
752
753 // Actually create the revision and create/update the page.
754 // Do NOT yet set $this->status!
755 if ( $flags & EDIT_UPDATE ) {
756 $status = $this->doModify( $summary, $this->user, $flags );
757 } else {
758 $status = $this->doCreate( $summary, $this->user, $flags );
759 }
760
761 // Promote user to any groups they meet the criteria for
762 DeferredUpdates::addCallableUpdate( function () use ( $user ) {
763 $user->addAutopromoteOnceGroups( 'onEdit' );
764 $user->addAutopromoteOnceGroups( 'onView' ); // b/c
765 } );
766
767 // NOTE: set $this->status only after all hooks have been called,
768 // so wasCommitted doesn't return true wehn called indirectly from a hook handler!
769 $this->status = $status;
770
771 // TODO: replace bad status with Exceptions!
772 return ( $this->status && $this->status->isOK() )
773 ? $this->status->value['revision-record']
774 : null;
775 }
776
782 public function wasCommitted() {
783 return $this->status !== null;
784 }
785
809 public function getStatus() {
810 return $this->status;
811 }
812
818 public function wasSuccessful() {
819 return $this->status && $this->status->isOK();
820 }
821
827 public function isNew() {
828 return $this->status && $this->status->isOK() && $this->status->value['new'];
829 }
830
838 public function isUnchanged() {
839 return $this->status
840 && $this->status->isOK()
841 && $this->status->value['revision-record'] === null;
842 }
843
850 public function getNewRevision() {
851 return ( $this->status && $this->status->isOK() )
852 ? $this->status->value['revision-record']
853 : null;
854 }
855
871 private function makeNewRevision(
872 CommentStoreComment $comment,
873 User $user,
874 $flags,
876 ) {
877 $wikiPage = $this->getWikiPage();
878 $title = $this->getTitle();
879 $parent = $this->grabParentRevision();
880
881 // XXX: we expect to get a MutableRevisionRecord here, but that's a bit brittle!
882 // TODO: introduce something like an UnsavedRevisionFactory service instead!
884 $rev = $this->derivedDataUpdater->getRevision();
885
886 $rev->setPageId( $title->getArticleID() );
887
888 if ( $parent ) {
889 $oldid = $parent->getId();
890 $rev->setParentId( $oldid );
891 } else {
892 $oldid = 0;
893 }
894
895 $rev->setComment( $comment );
896 $rev->setUser( $user );
897 $rev->setMinorEdit( ( $flags & EDIT_MINOR ) > 0 );
898
899 foreach ( $rev->getSlots()->getSlots() as $slot ) {
900 $content = $slot->getContent();
901
902 // XXX: We may push this up to the "edit controller" level, see T192777.
903 // XXX: prepareSave() and isValid() could live in SlotRoleHandler
904 // XXX: PrepareSave should not take a WikiPage!
905 $prepStatus = $content->prepareSave( $wikiPage, $flags, $oldid, $user );
906
907 // TODO: MCR: record which problem arose in which slot.
908 $status->merge( $prepStatus );
909 }
910
912 $rev->getSlotRoles(),
913 $status
914 );
915
916 return $rev;
917 }
918
927 private function doModify( CommentStoreComment $summary, User $user, $flags ) {
928 $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
929
930 // Update article, but only if changed.
931 $status = Status::newGood( [ 'new' => false, 'revision' => null, 'revision-record' => null ] );
932
933 $oldRev = $this->grabParentRevision();
934 $oldid = $oldRev ? $oldRev->getId() : 0;
935
936 if ( !$oldRev ) {
937 // Article gone missing
938 $status->fatal( 'edit-gone-missing' );
939
940 return $status;
941 }
942
943 $newRevisionRecord = $this->makeNewRevision(
944 $summary,
945 $user,
946 $flags,
947 $status
948 );
949
950 if ( !$status->isOK() ) {
951 return $status;
952 }
953
954 $now = $newRevisionRecord->getTimestamp();
955
956 // XXX: we may want a flag that allows a null revision to be forced!
957 $changed = $this->derivedDataUpdater->isChange();
958
959 $dbw = $this->getDBConnectionRef( DB_MASTER );
960
961 if ( $changed ) {
962 $dbw->startAtomic( __METHOD__ );
963
964 // Get the latest page_latest value while locking it.
965 // Do a CAS style check to see if it's the same as when this method
966 // started. If it changed then bail out before touching the DB.
967 $latestNow = $wikiPage->lockAndGetLatest(); // TODO: move to storage service, pass DB
968 if ( $latestNow != $oldid ) {
969 // We don't need to roll back, since we did not modify the database yet.
970 // XXX: Or do we want to rollback, any transaction started by calling
971 // code will fail? If we want that, we should probably throw an exception.
972 $dbw->endAtomic( __METHOD__ );
973 // Page updated or deleted in the mean time
974 $status->fatal( 'edit-conflict' );
975
976 return $status;
977 }
978
979 // At this point we are now comitted to returning an OK
980 // status unless some DB query error or other exception comes up.
981 // This way callers don't have to call rollback() if $status is bad
982 // unless they actually try to catch exceptions (which is rare).
983
984 // Save revision content and meta-data
985 $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
986 $newLegacyRevision = new Revision( $newRevisionRecord );
987
988 // Update page_latest and friends to reflect the new revision
989 // TODO: move to storage service
990 $wasRedirect = $this->derivedDataUpdater->wasRedirect();
991 if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, null, $wasRedirect ) ) {
992 throw new PageUpdateException( "Failed to update page row to use new revision." );
993 }
994
995 // TODO: replace legacy hook!
996 $tags = $this->computeEffectiveTags( $flags );
997 Hooks::run(
998 'NewRevisionFromEditComplete',
999 [ $wikiPage, $newLegacyRevision, $this->getOriginalRevisionId(), $user, &$tags ]
1000 );
1001
1002 // Update recentchanges
1003 if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1004 // Add RC row to the DB
1005 RecentChange::notifyEdit(
1006 $now,
1007 $this->getTitle(),
1008 $newRevisionRecord->isMinor(),
1009 $user,
1010 $summary->text, // TODO: pass object when that becomes possible
1011 $oldid,
1012 $newRevisionRecord->getTimestamp(),
1013 ( $flags & EDIT_FORCE_BOT ) > 0,
1014 '',
1015 $oldRev->getSize(),
1016 $newRevisionRecord->getSize(),
1017 $newRevisionRecord->getId(),
1018 $this->rcPatrolStatus,
1019 $tags
1020 );
1021 }
1022
1024
1025 $dbw->endAtomic( __METHOD__ );
1026
1027 // Return the new revision to the caller
1028 $status->value['revision-record'] = $newRevisionRecord;
1029
1030 // TODO: globally replace usages of 'revision' with getNewRevision()
1031 $status->value['revision'] = $newLegacyRevision;
1032 } else {
1033 // T34948: revision ID must be set to page {{REVISIONID}} and
1034 // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
1035 // Since we don't insert a new revision into the database, the least
1036 // error-prone way is to reuse given old revision.
1037 $newRevisionRecord = $oldRev;
1038
1039 $status->warning( 'edit-no-change' );
1040 // Update page_touched as updateRevisionOn() was not called.
1041 // Other cache updates are managed in WikiPage::onArticleEdit()
1042 // via WikiPage::doEditUpdates().
1043 $this->getTitle()->invalidateCache( $now );
1044 }
1045
1046 // Do secondary updates once the main changes have been committed...
1047 // NOTE: the updates have to be processed before sending the response to the client
1048 // (DeferredUpdates::PRESEND), otherwise the client may already be following the
1049 // HTTP redirect to the standard view before dervide data has been created - most
1050 // importantly, before the parser cache has been updated. This would cause the
1051 // content to be parsed a second time, or may cause stale content to be shown.
1052 DeferredUpdates::addUpdate(
1054 $dbw,
1055 $wikiPage,
1056 $newRevisionRecord,
1057 $user,
1058 $summary,
1059 $flags,
1060 $status,
1061 [ 'changed' => $changed, ]
1062 ),
1063 DeferredUpdates::PRESEND
1064 );
1065
1066 return $status;
1067 }
1068
1078 private function doCreate( CommentStoreComment $summary, User $user, $flags ) {
1079 $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
1080
1081 if ( !$this->derivedDataUpdater->getSlots()->hasSlot( SlotRecord::MAIN ) ) {
1082 throw new PageUpdateException( 'Must provide a main slot when creating a page!' );
1083 }
1084
1085 $status = Status::newGood( [ 'new' => true, 'revision' => null, 'revision-record' => null ] );
1086
1087 $newRevisionRecord = $this->makeNewRevision(
1088 $summary,
1089 $user,
1090 $flags,
1091 $status
1092 );
1093
1094 if ( !$status->isOK() ) {
1095 return $status;
1096 }
1097
1098 $now = $newRevisionRecord->getTimestamp();
1099
1100 $dbw = $this->getDBConnectionRef( DB_MASTER );
1101 $dbw->startAtomic( __METHOD__ );
1102
1103 // Add the page record unless one already exists for the title
1104 // TODO: move to storage service
1105 $newid = $wikiPage->insertOn( $dbw );
1106 if ( $newid === false ) {
1107 $dbw->endAtomic( __METHOD__ );
1108 $status->fatal( 'edit-already-exists' );
1109
1110 return $status;
1111 }
1112
1113 // At this point we are now comitted to returning an OK
1114 // status unless some DB query error or other exception comes up.
1115 // This way callers don't have to call rollback() if $status is bad
1116 // unless they actually try to catch exceptions (which is rare).
1117 $newRevisionRecord->setPageId( $newid );
1118
1119 // Save the revision text...
1120 $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
1121 $newLegacyRevision = new Revision( $newRevisionRecord );
1122
1123 // Update the page record with revision data
1124 // TODO: move to storage service
1125 if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, 0 ) ) {
1126 throw new PageUpdateException( "Failed to update page row to use new revision." );
1127 }
1128
1129 // TODO: replace legacy hook!
1130 $tags = $this->computeEffectiveTags( $flags );
1131 Hooks::run(
1132 'NewRevisionFromEditComplete',
1133 [ $wikiPage, $newLegacyRevision, false, $user, &$tags ]
1134 );
1135
1136 // Update recentchanges
1137 if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1138 // Add RC row to the DB
1139 RecentChange::notifyNew(
1140 $now,
1141 $this->getTitle(),
1142 $newRevisionRecord->isMinor(),
1143 $user,
1144 $summary->text, // TODO: pass object when that becomes possible
1145 ( $flags & EDIT_FORCE_BOT ) > 0,
1146 '',
1147 $newRevisionRecord->getSize(),
1148 $newRevisionRecord->getId(),
1149 $this->rcPatrolStatus,
1150 $tags
1151 );
1152 }
1153
1155
1156 if ( $this->usePageCreationLog ) {
1157 // Log the page creation
1158 // @TODO: Do we want a 'recreate' action?
1159 $logEntry = new ManualLogEntry( 'create', 'create' );
1160 $logEntry->setPerformer( $user );
1161 $logEntry->setTarget( $this->getTitle() );
1162 $logEntry->setComment( $summary->text );
1163 $logEntry->setTimestamp( $now );
1164 $logEntry->setAssociatedRevId( $newRevisionRecord->getId() );
1165 $logEntry->insert();
1166 // Note that we don't publish page creation events to recentchanges
1167 // (i.e. $logEntry->publish()) since this would create duplicate entries,
1168 // one for the edit and one for the page creation.
1169 }
1170
1171 $dbw->endAtomic( __METHOD__ );
1172
1173 // Return the new revision to the caller
1174 // TODO: globally replace usages of 'revision' with getNewRevision()
1175 $status->value['revision'] = $newLegacyRevision;
1176 $status->value['revision-record'] = $newRevisionRecord;
1177
1178 // Do secondary updates once the main changes have been committed...
1179 DeferredUpdates::addUpdate(
1181 $dbw,
1182 $wikiPage,
1183 $newRevisionRecord,
1184 $user,
1185 $summary,
1186 $flags,
1187 $status,
1188 [ 'created' => true ]
1189 ),
1190 DeferredUpdates::PRESEND
1191 );
1192
1193 return $status;
1194 }
1195
1196 private function getAtomicSectionUpdate(
1197 IDatabase $dbw,
1199 RevisionRecord $newRevisionRecord,
1200 User $user,
1201 CommentStoreComment $summary,
1202 $flags,
1204 $hints = []
1205 ) {
1206 return new AtomicSectionUpdate(
1207 $dbw,
1208 __METHOD__,
1209 function () use (
1210 $wikiPage, $newRevisionRecord, $user,
1211 $summary, $flags, $status, $hints
1212 ) {
1213 // set debug data
1214 $hints['causeAction'] = 'edit-page';
1215 $hints['causeAgent'] = $user->getName();
1216
1217 $newLegacyRevision = new Revision( $newRevisionRecord );
1218 $mainContent = $newRevisionRecord->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
1219
1220 // Update links tables, site stats, etc.
1221 $this->derivedDataUpdater->prepareUpdate( $newRevisionRecord, $hints );
1222 $this->derivedDataUpdater->doUpdates();
1223
1224 // TODO: replace legacy hook!
1225 // TODO: avoid pass-by-reference, see T193950
1226
1227 if ( $hints['created'] ?? false ) {
1228 // Trigger post-create hook
1229 $params = [ &$wikiPage, &$user, $mainContent, $summary->text,
1230 $flags & EDIT_MINOR, null, null, &$flags, $newLegacyRevision ];
1231 Hooks::run( 'PageContentInsertComplete', $params );
1232 }
1233
1234 // Trigger post-save hook
1235 $params = [ &$wikiPage, &$user, $mainContent, $summary->text,
1236 $flags & EDIT_MINOR, null, null, &$flags, $newLegacyRevision,
1238 Hooks::run( 'PageContentSaveComplete', $params );
1239 }
1240 );
1241 }
1242
1246 private function getRequiredSlotRoles() {
1247 return $this->slotRoleRegistry->getRequiredRoles( $this->getTitle() );
1248 }
1249
1253 private function getAllowedSlotRoles() {
1254 return $this->slotRoleRegistry->getAllowedRoles( $this->getTitle() );
1255 }
1256
1257 private function ensureRoleAllowed( $role ) {
1258 $allowedRoles = $this->getAllowedSlotRoles();
1259 if ( !in_array( $role, $allowedRoles ) ) {
1260 throw new PageUpdateException( "Slot role `$role` is not allowed." );
1261 }
1262 }
1263
1264 private function ensureRoleNotRequired( $role ) {
1265 $requiredRoles = $this->getRequiredSlotRoles();
1266 if ( in_array( $role, $requiredRoles ) ) {
1267 throw new PageUpdateException( "Slot role `$role` is required." );
1268 }
1269 }
1270
1271 private function checkAllRolesAllowed( array $roles, Status $status ) {
1272 $allowedRoles = $this->getAllowedSlotRoles();
1273
1274 $forbidden = array_diff( $roles, $allowedRoles );
1275 if ( !empty( $forbidden ) ) {
1276 $status->error(
1277 'edit-slots-cannot-add',
1278 count( $forbidden ),
1279 implode( ', ', $forbidden )
1280 );
1281 }
1282 }
1283
1284 private function checkNoRolesRequired( array $roles, Status $status ) {
1285 $requiredRoles = $this->getRequiredSlotRoles();
1286
1287 $needed = array_diff( $roles, $requiredRoles );
1288 if ( !empty( $needed ) ) {
1289 $status->error(
1290 'edit-slots-cannot-remove',
1291 count( $needed ),
1292 implode( ', ', $needed )
1293 );
1294 }
1295 }
1296
1297 private function checkAllRequiredRoles( array $roles, Status $status ) {
1298 $requiredRoles = $this->getRequiredSlotRoles();
1299
1300 $missing = array_diff( $requiredRoles, $roles );
1301 if ( !empty( $missing ) ) {
1302 $status->error(
1303 'edit-slots-missing',
1304 count( $missing ),
1305 implode( ', ', $missing )
1306 );
1307 }
1308 }
1309
1310}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
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:441
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.
getRole()
Returns the role of the slot.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
A handle for managing updates for derived page data on edit, import, purge, etc.
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...
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.
checkAllRolesAllowed(array $roles, Status $status)
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.
checkNoRolesRequired(array $roles, Status $status)
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,...
SlotRoleRegistry $slotRoleRegistry
setUsePageCreationLog( $use)
Whether to create a log entry for new page creations.
removeSlot( $role)
Removes the slot with the given role.
checkAllRequiredRoles(array $roles, Status $status)
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...
__construct(User $user, WikiPage $wikiPage, DerivedPageDataUpdater $derivedDataUpdater, LoadBalancer $loadBalancer, RevisionStore $revisionStore, SlotRoleRegistry $slotRoleRegistry)
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:40
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2452
addAutopromoteOnceGroups( $event)
Add the user to the group if he/she meets given criteria.
Definition User.php:1627
incEditCount()
Schedule a deferred update to update the user's edit count.
Definition User.php:5350
Class representing a MediaWiki article and history.
Definition WikiPage.php:45
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:14
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:165
const EDIT_INTERNAL
Definition Defines.php:168
const EDIT_UPDATE
Definition Defines.php:162
const EDIT_SUPPRESS_RC
Definition Defines.php:164
const EDIT_MINOR
Definition Defines.php:163
const EDIT_AUTOSUMMARY
Definition Defines.php:167
const EDIT_NEW
Definition Defines.php:161
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:894
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1266
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
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
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