MediaWiki  1.34.0
PageUpdater.php
Go to the documentation of this file.
1 <?php
25 namespace MediaWiki\Storage;
26 
28 use ChangeTags;
30 use Content;
31 use ContentHandler;
32 use DeferredUpdates;
33 use Hooks;
34 use LogicException;
35 use ManualLogEntry;
43 use MWException;
44 use RecentChange;
45 use Revision;
46 use RuntimeException;
47 use Status;
48 use Title;
49 use User;
50 use Wikimedia\Assert\Assert;
55 use WikiPage;
56 
72 class PageUpdater {
73 
77  private $user;
78 
82  private $wikiPage;
83 
88 
92  private $loadBalancer;
93 
97  private $revisionStore;
98 
103 
109 
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 
143  private $slotsUpdate;
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() {
410  return $this->originalRevId;
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 ) {
489  $tags = $this->tags;
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 ) {
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 ) {
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.
640  $this->checkAllRolesAllowed(
641  $this->slotsUpdate->getModifiedRoles(),
642  $status
643  );
644  $this->checkNoRolesRequired(
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 
688  $user = $this->user;
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 
912  $this->checkAllRequiredRoles(
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
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(),
1020  $tags
1021  );
1022  }
1023 
1024  $user->incEditCount();
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.
1054  $this->getAtomicSectionUpdate(
1055  $dbw,
1056  $wikiPage,
1057  $newRevisionRecord,
1058  $user,
1059  $summary,
1060  $flags,
1061  $status,
1062  [ 'changed' => $changed, ]
1063  ),
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
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(),
1151  $tags
1152  );
1153  }
1154 
1155  $user->incEditCount();
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...
1181  $this->getAtomicSectionUpdate(
1182  $dbw,
1183  $wikiPage,
1184  $newRevisionRecord,
1185  $user,
1186  $summary,
1187  $flags,
1188  $status,
1189  [ 'created' => true ]
1190  ),
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,
1204  Status $status,
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 }
MediaWiki\Storage\PageUpdater\addTags
addTags(array $tags)
Sets tags to apply to this update.
Definition: PageUpdater.php:468
MediaWiki\Storage\PageUpdater\makeNewRevision
makeNewRevision(CommentStoreComment $comment, User $user, $flags, Status $status)
Constructs a MutableRevisionRecord based on the Content prepared by the DerivedPageDataUpdater.
Definition: PageUpdater.php:871
ContentHandler
A content handler knows how do deal with a specific type of content on a wiki page.
Definition: ContentHandler.php:55
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:254
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
Revision\RevisionAccessException
Exception representing a failure to look up a revision.
Definition: RevisionAccessException.php:33
MediaWiki\Storage\PageUpdater\saveRevision
saveRevision(CommentStoreComment $summary, $flags=0)
Change an existing article or create a new article.
Definition: PageUpdater.php:624
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
MediaWiki\Storage\PageUpdater\$tags
array $tags
Definition: PageUpdater.php:133
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
MediaWiki\Storage\PageUpdater\setUndidRevisionId
setUndidRevisionId( $undidRevId)
Sets the ID of revision that was undone by the present update.
Definition: PageUpdater.php:446
MediaWiki\Storage\PageUpdater\setAjaxEditStash
setAjaxEditStash( $ajaxEditStash)
Definition: PageUpdater.php:216
MediaWiki\Storage\PageUpdater\doModify
doModify(CommentStoreComment $summary, User $user, $flags)
Definition: PageUpdater.php:928
MediaWiki\Storage\PageUpdater\$usePageCreationLog
bool $usePageCreationLog
whether to create a log entry for new page creations.
Definition: PageUpdater.php:118
WikiPage\updateRevisionOn
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1384
MediaWiki\Storage\PageUpdater\__construct
__construct(User $user, WikiPage $wikiPage, DerivedPageDataUpdater $derivedDataUpdater, ILoadBalancer $loadBalancer, RevisionStore $revisionStore, SlotRoleRegistry $slotRoleRegistry)
Definition: PageUpdater.php:158
Revision\SlotRecord\newInherited
static newInherited(SlotRecord $slot)
Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord of a p...
Definition: SlotRecord.php:103
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:136
MediaWiki\Storage\PageUpdater\checkNoRolesRequired
checkNoRolesRequired(array $roles, Status $status)
Definition: PageUpdater.php:1285
StatusValue\error
error( $message,... $parameters)
Add an error, do not set fatal flag This can be used for non-fatal errors.
Definition: StatusValue.php:193
EDIT_INTERNAL
const EDIT_INTERNAL
Definition: Defines.php:139
MediaWiki\Storage\PageUpdater\inheritSlot
inheritSlot(SlotRecord $originalSlot)
Explicitly inherit a slot from some earlier revision.
Definition: PageUpdater.php:379
User\incEditCount
incEditCount()
Schedule a deferred update to update the user's edit count.
Definition: User.php:4959
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:79
MediaWiki\Storage\PageUpdater\setOriginalRevisionId
setOriginalRevisionId( $originalRevId)
Sets the ID of an earlier revision that is being repeated or restored by this update.
Definition: PageUpdater.php:424
MediaWiki\Storage\PageUpdater\$ajaxEditStash
boolean $ajaxEditStash
see $wgAjaxEditStash
Definition: PageUpdater.php:123
MediaWiki\Storage\PageUpdater\getWikiId
getWikiId()
Definition: PageUpdater.php:220
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:70
MediaWiki\Storage\PageUpdater\$useAutomaticEditSummaries
boolean $useAutomaticEditSummaries
see $wgUseAutomaticEditSummaries
Definition: PageUpdater.php:108
MediaWiki\Storage\PageUpdater\getExplicitTags
getExplicitTags()
Returns the list of tags set using the addTag() method.
Definition: PageUpdater.php:480
StatusValue\warning
warning( $message,... $parameters)
Add a new warning.
Definition: StatusValue.php:178
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Definition: DeferredUpdates.php:85
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:47
StatusValue\fatal
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
Definition: StatusValue.php:208
MediaWiki\Storage\PageUpdater\setUsePageCreationLog
setUsePageCreationLog( $use)
Whether to create a log entry for new page creations.
Definition: PageUpdater.php:208
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1264
MediaWiki\Storage\PageUpdater\getParentContent
getParentContent( $role)
Returns the content of the given slot of the parent revision, with no audience checks applied.
Definition: PageUpdater.php:520
MediaWiki\Storage\PageUpdater\$wikiPage
WikiPage $wikiPage
Definition: PageUpdater.php:82
MediaWiki\Storage\PageUpdater\$loadBalancer
ILoadBalancer $loadBalancer
Definition: PageUpdater.php:92
MediaWiki\Storage\PageUpdater\wasCommitted
wasCommitted()
Whether saveRevision() has been called on this instance.
Definition: PageUpdater.php:782
MediaWiki\Storage\PageUpdater\getOriginalRevisionId
getOriginalRevisionId()
Returns the ID of an earlier revision that is being repeated or restored by this update.
Definition: PageUpdater.php:409
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
MediaWiki\Storage\PageUpdater\ensureRoleAllowed
ensureRoleAllowed( $role)
Definition: PageUpdater.php:1258
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:40
MediaWiki\Storage\PageUpdater\doCreate
doCreate(CommentStoreComment $summary, User $user, $flags)
Definition: PageUpdater.php:1079
Revision
Definition: Revision.php:40
MediaWiki\Storage\PageUpdater\makeAutoSummary
makeAutoSummary( $flags)
Definition: PageUpdater.php:556
MediaWiki\Storage\PageUpdater\$revisionStore
RevisionStore $revisionStore
Definition: PageUpdater.php:97
MediaWiki\Storage\PageUpdater\checkFlags
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
Definition: PageUpdater.php:334
MediaWiki\Storage\PageUpdater\wasSuccessful
wasSuccessful()
Whether saveRevision() completed successfully.
Definition: PageUpdater.php:818
MWException
MediaWiki exception.
Definition: MWException.php:26
MediaWiki\Storage\PageUpdater\setContent
setContent( $role, Content $content)
Set the new content for the given slot role.
Definition: PageUpdater.php:348
MediaWiki\Storage\PageUpdater\getTitle
getTitle()
Definition: PageUpdater.php:244
ChangeTags
Definition: ChangeTags.php:29
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:489
StatusValue\isOK
isOK()
Returns whether the operation completed.
Definition: StatusValue.php:130
WikiPage\insertOn
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1339
DeferredUpdates
Class for managing the deferred updates.
Definition: DeferredUpdates.php:62
MediaWiki\Storage\PageUpdater\getNewRevision
getNewRevision()
The new revision created by saveRevision(), or null if saveRevision() has not yet been called,...
Definition: PageUpdater.php:850
MediaWiki\Storage\PageUpdater\computeEffectiveTags
computeEffectiveTags( $flags)
Definition: PageUpdater.php:488
MediaWiki\Storage\PageUpdater\getAtomicSectionUpdate
getAtomicSectionUpdate(IDatabase $dbw, WikiPage $wikiPage, RevisionRecord $newRevisionRecord, User $user, CommentStoreComment $summary, $flags, Status $status, $hints=[])
Definition: PageUpdater.php:1197
MediaWiki\Storage\PageUpdater\getWikiPage
getWikiPage()
Definition: PageUpdater.php:252
RecentChange\notifyEdit
static notifyEdit( $timestamp, $title, $minor, $user, $comment, $oldId, $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to an edit.
Definition: RecentChange.php:632
StatusValue\merge
merge( $other, $overwriteValue=false)
Merge another status object into this one.
Definition: StatusValue.php:223
Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:60
ChangeTags\getSoftwareTags
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:58
$title
$title
Definition: testCompression.php:34
MediaWiki\Storage\PageUpdater\removeSlot
removeSlot( $role)
Removes the slot with the given role.
Definition: PageUpdater.php:397
DB_MASTER
const DB_MASTER
Definition: defines.php:26
MediaWiki\Storage\PageUpdater\getRequiredSlotRoles
getRequiredSlotRoles()
Definition: PageUpdater.php:1247
WikiPage\lockAndGetLatest
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2988
MediaWiki\Storage\PageUpdater\checkAllRolesAllowed
checkAllRolesAllowed(array $roles, Status $status)
Definition: PageUpdater.php:1272
MediaWiki\Storage\PageUpdater\addTag
addTag( $tag)
Sets a tag to apply to this update.
Definition: PageUpdater.php:457
MediaWiki\Storage\PageUpdater\getAllowedSlotRoles
getAllowedSlotRoles()
Definition: PageUpdater.php:1254
AtomicSectionUpdate
Deferrable Update for closure/callback updates via IDatabase::doAtomicSection()
Definition: AtomicSectionUpdate.php:9
MediaWiki\Storage\RevisionSlotsUpdate
Value object representing a modification of revision slots.
Definition: RevisionSlotsUpdate.php:36
$content
$content
Definition: router.php:78
MediaWiki\Storage\PageUpdater\$user
User $user
Definition: PageUpdater.php:77
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:133
ContentHandler\getLocalizedName
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
Definition: ContentHandler.php:318
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:42
MediaWiki\Storage\PageUpdater
Controller-like object for creating and updating pages by creating new revisions.
Definition: PageUpdater.php:72
MediaWiki\Storage\PageUpdater\getDBConnectionRef
getDBConnectionRef( $mode)
Definition: PageUpdater.php:229
MediaWiki\Storage\PageUpdater\hasEditConflict
hasEditConflict( $expectedParentRevision)
Checks whether this update conflicts with another update performed between the client loading data to...
Definition: PageUpdater.php:290
MediaWiki\Storage
Definition: BlobAccessException.php:23
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:41
Wikimedia\Rdbms\DBUnexpectedError
Definition: DBUnexpectedError.php:27
Content
Base interface for content objects.
Definition: Content.php:34
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:132
MediaWiki\Storage\PageUpdater\$status
Status null $status
Definition: PageUpdater.php:148
Wikimedia\Rdbms\DBConnRef
Helper class used for automatically marking an IDatabase connection as reusable (once it no longer ma...
Definition: DBConnRef.php:29
MediaWiki\Storage\PageUpdater\$slotsUpdate
RevisionSlotsUpdate $slotsUpdate
Definition: PageUpdater.php:143
Title
Represents a title within MediaWiki.
Definition: Title.php:42
EDIT_AUTOSUMMARY
const EDIT_AUTOSUMMARY
Definition: Defines.php:138
MediaWiki\Storage\PageUpdater\grabParentRevision
grabParentRevision()
Returns the revision that was the page's current revision when grabParentRevision() was first called.
Definition: PageUpdater.php:324
RecentChange\notifyNew
static notifyNew( $timestamp, $title, $minor, $user, $comment, $bot, $ip='', $size=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to page creation Note: the title object must be loaded w...
Definition: RecentChange.php:706
DeferredUpdates\PRESEND
const PRESEND
Definition: DeferredUpdates.php:69
MediaWiki\Storage\PageUpdater\getStatus
getStatus()
The Status object indicating whether saveRevision() was successful, or null if saveRevision() was not...
Definition: PageUpdater.php:809
MediaWiki\Storage\PageUpdater\setRcPatrolStatus
setRcPatrolStatus( $status)
Sets the "patrolled" status of the edit.
Definition: PageUpdater.php:197
MediaWiki\Storage\PageUpdater\$rcPatrolStatus
int $rcPatrolStatus
the RC patrol status the new revision should be marked with.
Definition: PageUpdater.php:113
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:79
MediaWiki\Storage\PageUpdater\ensureRoleNotRequired
ensureRoleNotRequired( $role)
Definition: PageUpdater.php:1265
MediaWiki\Storage\PageUpdater\getContentHandler
getContentHandler( $role)
Definition: PageUpdater.php:534
MediaWiki\Storage\PageUpdater\getUndidRevisionId
getUndidRevisionId()
Returns the revision ID set by setUndidRevisionId(), indicating what revision is being undone by this...
Definition: PageUpdater.php:435
MediaWiki\Storage\PageUpdater\$undidRevId
int $undidRevId
Definition: PageUpdater.php:138
Revision\RevisionRecord\getContent
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
Definition: RevisionRecord.php:167
User\addAutopromoteOnceGroups
addAutopromoteOnceGroups( $event)
Add the user to the group if he/she meets given criteria.
Definition: User.php:1494
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:37
MediaWiki\Storage\PageUpdater\$derivedDataUpdater
DerivedPageDataUpdater $derivedDataUpdater
Definition: PageUpdater.php:87
MediaWiki\Storage\PageUpdater\checkAllRequiredRoles
checkAllRequiredRoles(array $roles, Status $status)
Definition: PageUpdater.php:1298
Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:134
EDIT_SUPPRESS_RC
const EDIT_SUPPRESS_RC
Definition: Defines.php:135
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
MediaWiki\Storage\PageUpdateException
Exception representing a failure to update a page entry.
Definition: PageUpdateException.php:32
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:100
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:124
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
MediaWiki\Storage\PageUpdater\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: PageUpdater.php:102
MediaWiki\Storage\PageUpdater\$originalRevId
bool int $originalRevId
Definition: PageUpdater.php:128
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2232
CommentStoreComment
CommentStoreComment represents a comment stored by CommentStore.
Definition: CommentStoreComment.php:29
MediaWiki\Storage\PageUpdater\setUseAutomaticEditSummaries
setUseAutomaticEditSummaries( $useAutomaticEditSummaries)
Can be used to enable or disable automatic summaries that are applied to certain kinds of changes,...
Definition: PageUpdater.php:184
MediaWiki\Storage\PageUpdater\isNew
isNew()
Whether saveRevision() was called and created a new page.
Definition: PageUpdater.php:827
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
MediaWiki\Storage\PageUpdater\isUnchanged
isUnchanged()
Whether saveRevision() did not create a revision because the content didn't change (null-edit).
Definition: PageUpdater.php:838
MediaWiki\Storage\PageUpdater\setSlot
setSlot(SlotRecord $slot)
Set the new slot for the given slot role.
Definition: PageUpdater.php:359
Hooks
Hooks class.
Definition: Hooks.php:34
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
MediaWiki\Storage\PageUpdater\getLinkTarget
getLinkTarget()
Definition: PageUpdater.php:236