MediaWiki REL1_34
PageUpdater.php
Go to the documentation of this file.
1<?php
25namespace MediaWiki\Storage;
26
28use ChangeTags;
30use Content;
33use Hooks;
34use LogicException;
43use MWException;
44use RecentChange;
45use Revision;
46use RuntimeException;
47use Status;
48use Title;
49use User;
50use Wikimedia\Assert\Assert;
55use WikiPage;
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 '@phan-var MutableRevisionRecord $rev';
886
887 $rev->setPageId( $title->getArticleID() );
888
889 if ( $parent ) {
890 $oldid = $parent->getId();
891 $rev->setParentId( $oldid );
892 } else {
893 $oldid = 0;
894 }
895
896 $rev->setComment( $comment );
897 $rev->setUser( $user );
898 $rev->setMinorEdit( ( $flags & EDIT_MINOR ) > 0 );
899
900 foreach ( $rev->getSlots()->getSlots() as $slot ) {
901 $content = $slot->getContent();
902
903 // XXX: We may push this up to the "edit controller" level, see T192777.
904 // XXX: prepareSave() and isValid() could live in SlotRoleHandler
905 // XXX: PrepareSave should not take a WikiPage!
906 $prepStatus = $content->prepareSave( $wikiPage, $flags, $oldid, $user );
907
908 // TODO: MCR: record which problem arose in which slot.
909 $status->merge( $prepStatus );
910 }
911
913 $rev->getSlotRoles(),
914 $status
915 );
916
917 return $rev;
918 }
919
928 private function doModify( CommentStoreComment $summary, User $user, $flags ) {
929 $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
930
931 // Update article, but only if changed.
932 $status = Status::newGood( [ 'new' => false, 'revision' => null, 'revision-record' => null ] );
933
934 $oldRev = $this->grabParentRevision();
935 $oldid = $oldRev ? $oldRev->getId() : 0;
936
937 if ( !$oldRev ) {
938 // Article gone missing
939 $status->fatal( 'edit-gone-missing' );
940
941 return $status;
942 }
943
944 $newRevisionRecord = $this->makeNewRevision(
945 $summary,
946 $user,
947 $flags,
948 $status
949 );
950
951 if ( !$status->isOK() ) {
952 return $status;
953 }
954
955 $now = $newRevisionRecord->getTimestamp();
956
957 // XXX: we may want a flag that allows a null revision to be forced!
958 $changed = $this->derivedDataUpdater->isChange();
959
960 $dbw = $this->getDBConnectionRef( DB_MASTER );
961
962 if ( $changed ) {
963 $dbw->startAtomic( __METHOD__ );
964
965 // Get the latest page_latest value while locking it.
966 // Do a CAS style check to see if it's the same as when this method
967 // started. If it changed then bail out before touching the DB.
968 $latestNow = $wikiPage->lockAndGetLatest(); // TODO: move to storage service, pass DB
969 if ( $latestNow != $oldid ) {
970 // We don't need to roll back, since we did not modify the database yet.
971 // XXX: Or do we want to rollback, any transaction started by calling
972 // code will fail? If we want that, we should probably throw an exception.
973 $dbw->endAtomic( __METHOD__ );
974 // Page updated or deleted in the mean time
975 $status->fatal( 'edit-conflict' );
976
977 return $status;
978 }
979
980 // At this point we are now comitted to returning an OK
981 // status unless some DB query error or other exception comes up.
982 // This way callers don't have to call rollback() if $status is bad
983 // unless they actually try to catch exceptions (which is rare).
984
985 // Save revision content and meta-data
986 $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
987 $newLegacyRevision = new Revision( $newRevisionRecord );
988
989 // Update page_latest and friends to reflect the new revision
990 // TODO: move to storage service
991 $wasRedirect = $this->derivedDataUpdater->wasRedirect();
992 if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, null, $wasRedirect ) ) {
993 throw new PageUpdateException( "Failed to update page row to use new revision." );
994 }
995
996 // TODO: replace legacy hook!
997 $tags = $this->computeEffectiveTags( $flags );
998 Hooks::run(
999 'NewRevisionFromEditComplete',
1000 [ $wikiPage, $newLegacyRevision, $this->getOriginalRevisionId(), $user, &$tags ]
1001 );
1002
1003 // Update recentchanges
1004 if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1005 // Add RC row to the DB
1006 RecentChange::notifyEdit(
1007 $now,
1008 $this->getTitle(),
1009 $newRevisionRecord->isMinor(),
1010 $user,
1011 $summary->text, // TODO: pass object when that becomes possible
1012 $oldid,
1013 $newRevisionRecord->getTimestamp(),
1014 ( $flags & EDIT_FORCE_BOT ) > 0,
1015 '',
1016 $oldRev->getSize(),
1017 $newRevisionRecord->getSize(),
1018 $newRevisionRecord->getId(),
1019 $this->rcPatrolStatus,
1020 $tags
1021 );
1022 }
1023
1025
1026 $dbw->endAtomic( __METHOD__ );
1027
1028 // Return the new revision to the caller
1029 $status->value['revision-record'] = $newRevisionRecord;
1030
1031 // TODO: globally replace usages of 'revision' with getNewRevision()
1032 $status->value['revision'] = $newLegacyRevision;
1033 } else {
1034 // T34948: revision ID must be set to page {{REVISIONID}} and
1035 // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
1036 // Since we don't insert a new revision into the database, the least
1037 // error-prone way is to reuse given old revision.
1038 $newRevisionRecord = $oldRev;
1039
1040 $status->warning( 'edit-no-change' );
1041 // Update page_touched as updateRevisionOn() was not called.
1042 // Other cache updates are managed in WikiPage::onArticleEdit()
1043 // via WikiPage::doEditUpdates().
1044 $this->getTitle()->invalidateCache( $now );
1045 }
1046
1047 // Do secondary updates once the main changes have been committed...
1048 // NOTE: the updates have to be processed before sending the response to the client
1049 // (DeferredUpdates::PRESEND), otherwise the client may already be following the
1050 // HTTP redirect to the standard view before dervide data has been created - most
1051 // importantly, before the parser cache has been updated. This would cause the
1052 // content to be parsed a second time, or may cause stale content to be shown.
1053 DeferredUpdates::addUpdate(
1055 $dbw,
1056 $wikiPage,
1057 $newRevisionRecord,
1058 $user,
1059 $summary,
1060 $flags,
1061 $status,
1062 [ 'changed' => $changed, ]
1063 ),
1064 DeferredUpdates::PRESEND
1065 );
1066
1067 return $status;
1068 }
1069
1079 private function doCreate( CommentStoreComment $summary, User $user, $flags ) {
1080 $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
1081
1082 if ( !$this->derivedDataUpdater->getSlots()->hasSlot( SlotRecord::MAIN ) ) {
1083 throw new PageUpdateException( 'Must provide a main slot when creating a page!' );
1084 }
1085
1086 $status = Status::newGood( [ 'new' => true, 'revision' => null, 'revision-record' => null ] );
1087
1088 $newRevisionRecord = $this->makeNewRevision(
1089 $summary,
1090 $user,
1091 $flags,
1092 $status
1093 );
1094
1095 if ( !$status->isOK() ) {
1096 return $status;
1097 }
1098
1099 $now = $newRevisionRecord->getTimestamp();
1100
1101 $dbw = $this->getDBConnectionRef( DB_MASTER );
1102 $dbw->startAtomic( __METHOD__ );
1103
1104 // Add the page record unless one already exists for the title
1105 // TODO: move to storage service
1106 $newid = $wikiPage->insertOn( $dbw );
1107 if ( $newid === false ) {
1108 $dbw->endAtomic( __METHOD__ );
1109 $status->fatal( 'edit-already-exists' );
1110
1111 return $status;
1112 }
1113
1114 // At this point we are now comitted to returning an OK
1115 // status unless some DB query error or other exception comes up.
1116 // This way callers don't have to call rollback() if $status is bad
1117 // unless they actually try to catch exceptions (which is rare).
1118 $newRevisionRecord->setPageId( $newid );
1119
1120 // Save the revision text...
1121 $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
1122 $newLegacyRevision = new Revision( $newRevisionRecord );
1123
1124 // Update the page record with revision data
1125 // TODO: move to storage service
1126 if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, 0 ) ) {
1127 throw new PageUpdateException( "Failed to update page row to use new revision." );
1128 }
1129
1130 // TODO: replace legacy hook!
1131 $tags = $this->computeEffectiveTags( $flags );
1132 Hooks::run(
1133 'NewRevisionFromEditComplete',
1134 [ $wikiPage, $newLegacyRevision, false, $user, &$tags ]
1135 );
1136
1137 // Update recentchanges
1138 if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1139 // Add RC row to the DB
1140 RecentChange::notifyNew(
1141 $now,
1142 $this->getTitle(),
1143 $newRevisionRecord->isMinor(),
1144 $user,
1145 $summary->text, // TODO: pass object when that becomes possible
1146 ( $flags & EDIT_FORCE_BOT ) > 0,
1147 '',
1148 $newRevisionRecord->getSize(),
1149 $newRevisionRecord->getId(),
1150 $this->rcPatrolStatus,
1151 $tags
1152 );
1153 }
1154
1156
1157 if ( $this->usePageCreationLog ) {
1158 // Log the page creation
1159 // @TODO: Do we want a 'recreate' action?
1160 $logEntry = new ManualLogEntry( 'create', 'create' );
1161 $logEntry->setPerformer( $user );
1162 $logEntry->setTarget( $this->getTitle() );
1163 $logEntry->setComment( $summary->text );
1164 $logEntry->setTimestamp( $now );
1165 $logEntry->setAssociatedRevId( $newRevisionRecord->getId() );
1166 $logEntry->insert();
1167 // Note that we don't publish page creation events to recentchanges
1168 // (i.e. $logEntry->publish()) since this would create duplicate entries,
1169 // one for the edit and one for the page creation.
1170 }
1171
1172 $dbw->endAtomic( __METHOD__ );
1173
1174 // Return the new revision to the caller
1175 // TODO: globally replace usages of 'revision' with getNewRevision()
1176 $status->value['revision'] = $newLegacyRevision;
1177 $status->value['revision-record'] = $newRevisionRecord;
1178
1179 // Do secondary updates once the main changes have been committed...
1180 DeferredUpdates::addUpdate(
1182 $dbw,
1183 $wikiPage,
1184 $newRevisionRecord,
1185 $user,
1186 $summary,
1187 $flags,
1188 $status,
1189 [ 'created' => true ]
1190 ),
1191 DeferredUpdates::PRESEND
1192 );
1193
1194 return $status;
1195 }
1196
1197 private function getAtomicSectionUpdate(
1198 IDatabase $dbw,
1200 RevisionRecord $newRevisionRecord,
1201 User $user,
1202 CommentStoreComment $summary,
1203 $flags,
1205 $hints = []
1206 ) {
1207 return new AtomicSectionUpdate(
1208 $dbw,
1209 __METHOD__,
1210 function () use (
1211 $wikiPage, $newRevisionRecord, $user,
1212 $summary, $flags, $status, $hints
1213 ) {
1214 // set debug data
1215 $hints['causeAction'] = 'edit-page';
1216 $hints['causeAgent'] = $user->getName();
1217
1218 $newLegacyRevision = new Revision( $newRevisionRecord );
1219 $mainContent = $newRevisionRecord->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
1220
1221 // Update links tables, site stats, etc.
1222 $this->derivedDataUpdater->prepareUpdate( $newRevisionRecord, $hints );
1223 $this->derivedDataUpdater->doUpdates();
1224
1225 // TODO: replace legacy hook!
1226 // TODO: avoid pass-by-reference, see T193950
1227
1228 if ( $hints['created'] ?? false ) {
1229 // Trigger post-create hook
1230 $params = [ &$wikiPage, &$user, $mainContent, $summary->text,
1231 $flags & EDIT_MINOR, null, null, &$flags, $newLegacyRevision ];
1232 Hooks::run( 'PageContentInsertComplete', $params );
1233 }
1234
1235 // Trigger post-save hook
1236 $params = [ &$wikiPage, &$user, $mainContent, $summary->text,
1237 $flags & EDIT_MINOR, null, null, &$flags, $newLegacyRevision,
1239 Hooks::run( 'PageContentSaveComplete', $params );
1240 }
1241 );
1242 }
1243
1247 private function getRequiredSlotRoles() {
1248 return $this->slotRoleRegistry->getRequiredRoles( $this->getTitle() );
1249 }
1250
1254 private function getAllowedSlotRoles() {
1255 return $this->slotRoleRegistry->getAllowedRoles( $this->getTitle() );
1256 }
1257
1258 private function ensureRoleAllowed( $role ) {
1259 $allowedRoles = $this->getAllowedSlotRoles();
1260 if ( !in_array( $role, $allowedRoles ) ) {
1261 throw new PageUpdateException( "Slot role `$role` is not allowed." );
1262 }
1263 }
1264
1265 private function ensureRoleNotRequired( $role ) {
1266 $requiredRoles = $this->getRequiredSlotRoles();
1267 if ( in_array( $role, $requiredRoles ) ) {
1268 throw new PageUpdateException( "Slot role `$role` is required." );
1269 }
1270 }
1271
1272 private function checkAllRolesAllowed( array $roles, Status $status ) {
1273 $allowedRoles = $this->getAllowedSlotRoles();
1274
1275 $forbidden = array_diff( $roles, $allowedRoles );
1276 if ( !empty( $forbidden ) ) {
1277 $status->error(
1278 'edit-slots-cannot-add',
1279 count( $forbidden ),
1280 implode( ', ', $forbidden )
1281 );
1282 }
1283 }
1284
1285 private function checkNoRolesRequired( array $roles, Status $status ) {
1286 $requiredRoles = $this->getRequiredSlotRoles();
1287
1288 $needed = array_diff( $roles, $requiredRoles );
1289 if ( !empty( $needed ) ) {
1290 $status->error(
1291 'edit-slots-cannot-remove',
1292 count( $needed ),
1293 implode( ', ', $needed )
1294 );
1295 }
1296 }
1297
1298 private function checkAllRequiredRoles( array $roles, Status $status ) {
1299 $requiredRoles = $this->getRequiredSlotRoles();
1300
1301 $missing = array_diff( $requiredRoles, $roles );
1302 if ( !empty( $missing ) ) {
1303 $status->error(
1304 'edit-slots-missing',
1305 count( $missing ),
1306 implode( ', ', $missing )
1307 );
1308 }
1309 }
1310
1311}
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
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.
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...
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
__construct(User $user, WikiPage $wikiPage, DerivedPageDataUpdater $derivedDataUpdater, ILoadBalancer $loadBalancer, RevisionStore $revisionStore, SlotRoleRegistry $slotRoleRegistry)
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.
isOK()
Returns whether the operation completed.
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
merge( $other, $overwriteValue=false)
Merge another status object into this one.
error( $message,... $parameters)
Add an error, do not set fatal flag This can be used for non-fatal errors.
warning( $message,... $parameters)
Add a new warning.
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:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2364
addAutopromoteOnceGroups( $event)
Add the user to the group if he/she meets given criteria.
Definition User.php:1535
incEditCount()
Schedule a deferred update to update the user's edit count.
Definition User.php:5065
Class representing a MediaWiki article and history.
Definition WikiPage.php:47
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 used for automatically marking an IDatabase connection as reusable (once it no longer ma...
Definition DBConnRef.php:29
const EDIT_FORCE_BOT
Definition Defines.php:145
const EDIT_INTERNAL
Definition Defines.php:148
const EDIT_UPDATE
Definition Defines.php:142
const EDIT_SUPPRESS_RC
Definition Defines.php:144
const EDIT_MINOR
Definition Defines.php:143
const EDIT_AUTOSUMMARY
Definition Defines.php:147
const EDIT_NEW
Definition Defines.php:141
Base interface for content objects.
Definition Content.php:34
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_MASTER
Definition defines.php:26
$content
Definition router.php:78