MediaWiki  master
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;
47 use Status;
48 use Title;
49 use User;
55 use WikiPage;
56 
73 class PageUpdater {
74 
78  private $user;
79 
83  private $wikiPage;
84 
89 
93  private $loadBalancer;
94 
98  private $revisionStore;
99 
104 
110 
115 
119  private $usePageCreationLog = true;
120 
124  private $ajaxEditStash = true;
125 
129  private $originalRevId = false;
130 
134  private $tags = [];
135 
139  private $undidRevId = 0;
140 
144  private $slotsUpdate;
145 
149  private $status = null;
150 
159  public function __construct(
160  User $user,
166  ) {
167  $this->user = $user;
168  $this->wikiPage = $wikiPage;
169  $this->derivedDataUpdater = $derivedDataUpdater;
170 
171  $this->loadBalancer = $loadBalancer;
172  $this->revisionStore = $revisionStore;
173  $this->slotRoleRegistry = $slotRoleRegistry;
174 
175  $this->slotsUpdate = new RevisionSlotsUpdate();
176  }
177 
186  $this->useAutomaticEditSummaries = $useAutomaticEditSummaries;
187  }
188 
198  public function setRcPatrolStatus( $status ) {
199  $this->rcPatrolStatus = $status;
200  }
201 
209  public function setUsePageCreationLog( $use ) {
210  $this->usePageCreationLog = $use;
211  }
212 
217  public function setAjaxEditStash( $ajaxEditStash ) {
218  $this->ajaxEditStash = $ajaxEditStash;
219  }
220 
221  private function getWikiId() {
222  return false; // TODO: get from RevisionStore!
223  }
224 
230  private function getDBConnectionRef( $mode ) {
231  return $this->loadBalancer->getConnectionRef( $mode, [], $this->getWikiId() );
232  }
233 
237  private function getLinkTarget() {
238  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
239  return $this->wikiPage->getTitle();
240  }
241 
245  private function getTitle() {
246  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
247  return $this->wikiPage->getTitle();
248  }
249 
253  private function getWikiPage() {
254  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
255  return $this->wikiPage;
256  }
257 
291  public function hasEditConflict( $expectedParentRevision ) {
292  $parent = $this->grabParentRevision();
293  $parentId = $parent ? $parent->getId() : 0;
294 
295  return $parentId !== $expectedParentRevision;
296  }
297 
325  public function grabParentRevision() {
326  return $this->derivedDataUpdater->grabCurrentRevision();
327  }
328 
335  private function checkFlags( $flags ) {
336  if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
337  $flags |= ( $this->derivedDataUpdater->pageExisted() ) ? EDIT_UPDATE : EDIT_NEW;
338  }
339 
340  return $flags;
341  }
342 
349  public function setContent( $role, Content $content ) {
350  $this->ensureRoleAllowed( $role );
351 
352  $this->slotsUpdate->modifyContent( $role, $content );
353  }
354 
360  public function setSlot( SlotRecord $slot ) {
361  $this->ensureRoleAllowed( $slot->getRole() );
362 
363  $this->slotsUpdate->modifySlot( $slot );
364  }
365 
380  public function inheritSlot( SlotRecord $originalSlot ) {
381  // NOTE: slots can be inherited even if the role is not "allowed" on the title.
382  // NOTE: this slot is inherited from some other revision, but it's
383  // a "modified" slot for the RevisionSlotsUpdate and DerivedPageDataUpdater,
384  // since it's not implicitly inherited from the parent revision.
385  $inheritedSlot = SlotRecord::newInherited( $originalSlot );
386  $this->slotsUpdate->modifySlot( $inheritedSlot );
387  }
388 
398  public function removeSlot( $role ) {
399  $this->ensureRoleNotRequired( $role );
400 
401  $this->slotsUpdate->removeSlot( $role );
402  }
403 
410  public function getOriginalRevisionId() {
411  return $this->originalRevId;
412  }
413 
426  Assert::parameterType( 'integer|boolean', $originalRevId, '$originalRevId' );
427  $this->originalRevId = $originalRevId;
428  }
429 
436  public function getUndidRevisionId() {
437  return $this->undidRevId;
438  }
439 
447  public function setUndidRevisionId( $undidRevId ) {
448  Assert::parameterType( 'integer', $undidRevId, '$undidRevId' );
449  $this->undidRevId = $undidRevId;
450  }
451 
458  public function addTag( $tag ) {
459  Assert::parameterType( 'string', $tag, '$tag' );
460  $this->tags[] = trim( $tag );
461  }
462 
469  public function addTags( array $tags ) {
470  Assert::parameterElementType( 'string', $tags, '$tags' );
471  foreach ( $tags as $tag ) {
472  $this->addTag( $tag );
473  }
474  }
475 
481  public function getExplicitTags() {
482  return $this->tags;
483  }
484 
489  private function computeEffectiveTags( $flags ) {
490  $tags = $this->tags;
491 
492  foreach ( $this->slotsUpdate->getModifiedRoles() as $role ) {
493  $old_content = $this->getParentContent( $role );
494 
495  $handler = $this->getContentHandler( $role );
496  $content = $this->slotsUpdate->getModifiedSlot( $role )->getContent();
497 
498  // TODO: MCR: Do this for all slots. Also add tags for removing roles!
499  $tag = $handler->getChangeTag( $old_content, $content, $flags );
500  // If there is no applicable tag, null is returned, so we need to check
501  if ( $tag ) {
502  $tags[] = $tag;
503  }
504  }
505 
506  // Check for undo tag
507  if ( $this->undidRevId !== 0 && in_array( 'mw-undo', ChangeTags::getSoftwareTags() ) ) {
508  $tags[] = 'mw-undo';
509  }
510 
511  return array_unique( $tags );
512  }
513 
521  private function getParentContent( $role ) {
522  $parent = $this->grabParentRevision();
523 
524  if ( $parent && $parent->hasSlot( $role ) ) {
525  return $parent->getContent( $role, RevisionRecord::RAW );
526  }
527 
528  return null;
529  }
530 
535  private function getContentHandler( $role ) {
536  // TODO: inject something like a ContentHandlerRegistry
537  if ( $this->slotsUpdate->isModifiedSlot( $role ) ) {
538  $slot = $this->slotsUpdate->getModifiedSlot( $role );
539  } else {
540  $parent = $this->grabParentRevision();
541 
542  if ( $parent ) {
543  $slot = $parent->getSlot( $role, RevisionRecord::RAW );
544  } else {
545  throw new RevisionAccessException( 'No such slot: ' . $role );
546  }
547  }
548 
549  return ContentHandler::getForModelID( $slot->getModel() );
550  }
551 
557  private function makeAutoSummary( $flags ) {
558  if ( !$this->useAutomaticEditSummaries || ( $flags & EDIT_AUTOSUMMARY ) === 0 ) {
560  }
561 
562  // NOTE: this generates an auto-summary for SOME RANDOM changed slot!
563  // TODO: combine auto-summaries for multiple slots!
564  // XXX: this logic should not be in the storage layer!
565  $roles = $this->slotsUpdate->getModifiedRoles();
566  $role = reset( $roles );
567 
568  if ( $role === false ) {
570  }
571 
572  $handler = $this->getContentHandler( $role );
573  $content = $this->slotsUpdate->getModifiedSlot( $role )->getContent();
574  $old_content = $this->getParentContent( $role );
575  $summary = $handler->getAutosummary( $old_content, $content, $flags );
576 
577  return CommentStoreComment::newUnsavedComment( $summary );
578  }
579 
625  public function saveRevision( CommentStoreComment $summary, $flags = 0 ) {
626  // Defend against mistakes caused by differences with the
627  // signature of WikiPage::doEditContent.
628  Assert::parameterType( 'integer', $flags, '$flags' );
629 
630  if ( $this->wasCommitted() ) {
631  throw new RuntimeException( 'saveRevision() has already been called on this PageUpdater!' );
632  }
633 
634  // Low-level sanity check
635  if ( $this->getLinkTarget()->getText() === '' ) {
636  throw new RuntimeException( 'Something is trying to edit an article with an empty title' );
637  }
638 
639  // NOTE: slots can be inherited even if the role is not "allowed" on the title.
641  $this->checkAllRolesAllowed(
642  $this->slotsUpdate->getModifiedRoles(),
643  $status
644  );
645  $this->checkNoRolesRequired(
646  $this->slotsUpdate->getRemovedRoles(),
647  $status
648  );
649 
650  if ( !$status->isOK() ) {
651  return null;
652  }
653 
654  // Make sure the given content is allowed in the respective slots of this page
655  foreach ( $this->slotsUpdate->getModifiedRoles() as $role ) {
656  $slot = $this->slotsUpdate->getModifiedSlot( $role );
657  $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
658 
659  if ( !$roleHandler->isAllowedModel( $slot->getModel(), $this->getTitle() ) ) {
660  $contentHandler = ContentHandler::getForModelID( $slot->getModel() );
661  $this->status = Status::newFatal( 'content-not-allowed-here',
662  ContentHandler::getLocalizedName( $contentHandler->getModelID() ),
663  $this->getTitle()->getPrefixedText(),
664  wfMessage( $roleHandler->getNameMessageKey() )
665  // TODO: defer message lookup to caller
666  );
667  return null;
668  }
669  }
670 
671  // Load the data from the master database if needed. Needed to check flags.
672  // NOTE: This grabs the parent revision as the CAS token, if grabParentRevision
673  // wasn't called yet. If the page is modified by another process before we are done with
674  // it, this method must fail (with status 'edit-conflict')!
675  // NOTE: The parent revision may be different from $this->originalRevisionId.
676  $this->grabParentRevision();
677  $flags = $this->checkFlags( $flags );
678 
679  // Avoid statsd noise and wasted cycles check the edit stash (T136678)
680  if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
681  $useStashed = false;
682  } else {
683  $useStashed = $this->ajaxEditStash;
684  }
685 
686  // TODO: use this only for the legacy hook, and only if something uses the legacy hook
687  $wikiPage = $this->getWikiPage();
688 
689  $user = $this->user;
690 
691  // Prepare the update. This performs PST and generates the canonical ParserOutput.
692  $this->derivedDataUpdater->prepareContent(
693  $this->user,
694  $this->slotsUpdate,
695  $useStashed
696  );
697 
698  // TODO: don't force initialization here!
699  // This is a hack to work around the fact that late initialization of the ParserOutput
700  // causes ApiFlowEditHeaderTest::testCache to fail. Whether that failure indicates an
701  // actual problem, or is just an issue with the test setup, remains to be determined
702  // [dk, 2018-03].
703  // Anomie said in 2018-03:
704  /*
705  I suspect that what's breaking is this:
706 
707  The old version of WikiPage::doEditContent() called prepareContentForEdit() which
708  generated the ParserOutput right then, so when doEditUpdates() gets called from the
709  DeferredUpdate scheduled by WikiPage::doCreate() there's no need to parse. I note
710  there's a comment there that says "Get the pre-save transform content and final
711  parser output".
712  The new version of WikiPage::doEditContent() makes a PageUpdater and calls its
713  saveRevision(), which calls DerivedPageDataUpdater::prepareContent() and
714  PageUpdater::doCreate() without ever having to actually generate a ParserOutput.
715  Thus, when DerivedPageDataUpdater::doUpdates() is called from the DeferredUpdate
716  scheduled by PageUpdater::doCreate(), it does find that it needs to parse at that point.
717 
718  And the order of operations in that Flow test is presumably:
719 
720  - Create a page with a call to WikiPage::doEditContent(), in a way that somehow avoids
721  processing the DeferredUpdate.
722  - Set up the "no set!" mock cache in Flow\Tests\Api\ApiTestCase::expectCacheInvalidate()
723  - Then, during the course of doing that test, a $db->commit() results in the
724  DeferredUpdates being run.
725  */
726  $this->derivedDataUpdater->getCanonicalParserOutput();
727 
728  $mainContent = $this->derivedDataUpdater->getSlots()->getContent( SlotRecord::MAIN );
729 
730  // Trigger pre-save hook (using provided edit summary)
731  $hookStatus = Status::newGood( [] );
732  // TODO: replace legacy hook!
733  // TODO: avoid pass-by-reference, see T193950
734  $hook_args = [ &$wikiPage, &$user, &$mainContent, &$summary,
735  $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
736  // Check if the hook rejected the attempted save
737  if ( !Hooks::run( 'PageContentSave', $hook_args ) ) {
738  if ( $hookStatus->isOK() ) {
739  // Hook returned false but didn't call fatal(); use generic message
740  $hookStatus->fatal( 'edit-hook-aborted' );
741  }
742 
743  $this->status = $hookStatus;
744  return null;
745  }
746 
747  // Provide autosummaries if one is not provided and autosummaries are enabled
748  // XXX: $summary == null seems logical, but the empty string may actually come from the user
749  // XXX: Move this logic out of the storage layer! It does not belong here! Use a callback?
750  if ( $summary->text === '' && $summary->data === null ) {
751  $summary = $this->makeAutoSummary( $flags );
752  }
753 
754  // Actually create the revision and create/update the page.
755  // Do NOT yet set $this->status!
756  if ( $flags & EDIT_UPDATE ) {
757  $status = $this->doModify( $summary, $this->user, $flags );
758  } else {
759  $status = $this->doCreate( $summary, $this->user, $flags );
760  }
761 
762  // Promote user to any groups they meet the criteria for
763  DeferredUpdates::addCallableUpdate( function () use ( $user ) {
764  $user->addAutopromoteOnceGroups( 'onEdit' );
765  $user->addAutopromoteOnceGroups( 'onView' ); // b/c
766  } );
767 
768  // NOTE: set $this->status only after all hooks have been called,
769  // so wasCommitted doesn't return true wehn called indirectly from a hook handler!
770  $this->status = $status;
771 
772  // TODO: replace bad status with Exceptions!
773  return ( $this->status && $this->status->isOK() )
774  ? $this->status->value['revision-record']
775  : null;
776  }
777 
783  public function wasCommitted() {
784  return $this->status !== null;
785  }
786 
810  public function getStatus() {
811  return $this->status;
812  }
813 
819  public function wasSuccessful() {
820  return $this->status && $this->status->isOK();
821  }
822 
828  public function isNew() {
829  return $this->status && $this->status->isOK() && $this->status->value['new'];
830  }
831 
839  public function isUnchanged() {
840  return $this->status
841  && $this->status->isOK()
842  && $this->status->value['revision-record'] === null;
843  }
844 
851  public function getNewRevision() {
852  return ( $this->status && $this->status->isOK() )
853  ? $this->status->value['revision-record']
854  : null;
855  }
856 
872  private function makeNewRevision(
873  CommentStoreComment $comment,
874  User $user,
875  $flags,
877  ) {
878  $wikiPage = $this->getWikiPage();
879  $title = $this->getTitle();
880  $parent = $this->grabParentRevision();
881 
882  // XXX: we expect to get a MutableRevisionRecord here, but that's a bit brittle!
883  // TODO: introduce something like an UnsavedRevisionFactory service instead!
885  $rev = $this->derivedDataUpdater->getRevision();
886  '@phan-var MutableRevisionRecord $rev';
887 
888  $rev->setPageId( $title->getArticleID() );
889 
890  if ( $parent ) {
891  $oldid = $parent->getId();
892  $rev->setParentId( $oldid );
893  } else {
894  $oldid = 0;
895  }
896 
897  $rev->setComment( $comment );
898  $rev->setUser( $user );
899  $rev->setMinorEdit( ( $flags & EDIT_MINOR ) > 0 );
900 
901  foreach ( $rev->getSlots()->getSlots() as $slot ) {
902  $content = $slot->getContent();
903 
904  // XXX: We may push this up to the "edit controller" level, see T192777.
905  // XXX: prepareSave() and isValid() could live in SlotRoleHandler
906  // XXX: PrepareSave should not take a WikiPage!
907  $prepStatus = $content->prepareSave( $wikiPage, $flags, $oldid, $user );
908 
909  // TODO: MCR: record which problem arose in which slot.
910  $status->merge( $prepStatus );
911  }
912 
913  $this->checkAllRequiredRoles(
914  $rev->getSlotRoles(),
915  $status
916  );
917 
918  return $rev;
919  }
920 
929  private function doModify( CommentStoreComment $summary, User $user, $flags ) {
930  $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
931 
932  // Update article, but only if changed.
933  $status = Status::newGood( [ 'new' => false, 'revision' => null, 'revision-record' => null ] );
934 
935  $oldRev = $this->grabParentRevision();
936  $oldid = $oldRev ? $oldRev->getId() : 0;
937 
938  if ( !$oldRev ) {
939  // Article gone missing
940  $status->fatal( 'edit-gone-missing' );
941 
942  return $status;
943  }
944 
945  $newRevisionRecord = $this->makeNewRevision(
946  $summary,
947  $user,
948  $flags,
949  $status
950  );
951 
952  if ( !$status->isOK() ) {
953  return $status;
954  }
955 
956  $now = $newRevisionRecord->getTimestamp();
957 
958  // XXX: we may want a flag that allows a null revision to be forced!
959  $changed = $this->derivedDataUpdater->isChange();
960 
961  $dbw = $this->getDBConnectionRef( DB_MASTER );
962 
963  if ( $changed ) {
964  $dbw->startAtomic( __METHOD__ );
965 
966  // Get the latest page_latest value while locking it.
967  // Do a CAS style check to see if it's the same as when this method
968  // started. If it changed then bail out before touching the DB.
969  $latestNow = $wikiPage->lockAndGetLatest(); // TODO: move to storage service, pass DB
970  if ( $latestNow != $oldid ) {
971  // We don't need to roll back, since we did not modify the database yet.
972  // XXX: Or do we want to rollback, any transaction started by calling
973  // code will fail? If we want that, we should probably throw an exception.
974  $dbw->endAtomic( __METHOD__ );
975  // Page updated or deleted in the mean time
976  $status->fatal( 'edit-conflict' );
977 
978  return $status;
979  }
980 
981  // At this point we are now comitted to returning an OK
982  // status unless some DB query error or other exception comes up.
983  // This way callers don't have to call rollback() if $status is bad
984  // unless they actually try to catch exceptions (which is rare).
985 
986  // Save revision content and meta-data
987  $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
988  $newLegacyRevision = new Revision( $newRevisionRecord );
989 
990  // Update page_latest and friends to reflect the new revision
991  // TODO: move to storage service
992  $wasRedirect = $this->derivedDataUpdater->wasRedirect();
993  if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, null, $wasRedirect ) ) {
994  throw new PageUpdateException( "Failed to update page row to use new revision." );
995  }
996 
997  // TODO: replace legacy hook!
998  $tags = $this->computeEffectiveTags( $flags );
999  Hooks::run(
1000  'NewRevisionFromEditComplete',
1001  [ $wikiPage, $newLegacyRevision, $this->getOriginalRevisionId(), $user, &$tags ]
1002  );
1003 
1004  // Update recentchanges
1005  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1006  // Add RC row to the DB
1008  $now,
1009  $this->getTitle(),
1010  $newRevisionRecord->isMinor(),
1011  $user,
1012  $summary->text, // TODO: pass object when that becomes possible
1013  $oldid,
1014  $newRevisionRecord->getTimestamp(),
1015  ( $flags & EDIT_FORCE_BOT ) > 0,
1016  '',
1017  $oldRev->getSize(),
1018  $newRevisionRecord->getSize(),
1019  $newRevisionRecord->getId(),
1021  $tags
1022  );
1023  }
1024 
1025  $user->incEditCount();
1026 
1027  $dbw->endAtomic( __METHOD__ );
1028 
1029  // Return the new revision to the caller
1030  $status->value['revision-record'] = $newRevisionRecord;
1031 
1032  // TODO: globally replace usages of 'revision' with getNewRevision()
1033  $status->value['revision'] = $newLegacyRevision;
1034  } else {
1035  // T34948: revision ID must be set to page {{REVISIONID}} and
1036  // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
1037  // Since we don't insert a new revision into the database, the least
1038  // error-prone way is to reuse given old revision.
1039  $newRevisionRecord = $oldRev;
1040 
1041  $status->warning( 'edit-no-change' );
1042  // Update page_touched as updateRevisionOn() was not called.
1043  // Other cache updates are managed in WikiPage::onArticleEdit()
1044  // via WikiPage::doEditUpdates().
1045  $this->getTitle()->invalidateCache( $now );
1046  }
1047 
1048  // Do secondary updates once the main changes have been committed...
1049  // NOTE: the updates have to be processed before sending the response to the client
1050  // (DeferredUpdates::PRESEND), otherwise the client may already be following the
1051  // HTTP redirect to the standard view before dervide data has been created - most
1052  // importantly, before the parser cache has been updated. This would cause the
1053  // content to be parsed a second time, or may cause stale content to be shown.
1055  $this->getAtomicSectionUpdate(
1056  $dbw,
1057  $wikiPage,
1058  $newRevisionRecord,
1059  $user,
1060  $summary,
1061  $flags,
1062  $status,
1063  [ 'changed' => $changed, ]
1064  ),
1066  );
1067 
1068  return $status;
1069  }
1070 
1080  private function doCreate( CommentStoreComment $summary, User $user, $flags ) {
1081  $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
1082 
1083  if ( !$this->derivedDataUpdater->getSlots()->hasSlot( SlotRecord::MAIN ) ) {
1084  throw new PageUpdateException( 'Must provide a main slot when creating a page!' );
1085  }
1086 
1087  $status = Status::newGood( [ 'new' => true, 'revision' => null, 'revision-record' => null ] );
1088 
1089  $newRevisionRecord = $this->makeNewRevision(
1090  $summary,
1091  $user,
1092  $flags,
1093  $status
1094  );
1095 
1096  if ( !$status->isOK() ) {
1097  return $status;
1098  }
1099 
1100  $now = $newRevisionRecord->getTimestamp();
1101 
1102  $dbw = $this->getDBConnectionRef( DB_MASTER );
1103  $dbw->startAtomic( __METHOD__ );
1104 
1105  // Add the page record unless one already exists for the title
1106  // TODO: move to storage service
1107  $newid = $wikiPage->insertOn( $dbw );
1108  if ( $newid === false ) {
1109  $dbw->endAtomic( __METHOD__ );
1110  $status->fatal( 'edit-already-exists' );
1111 
1112  return $status;
1113  }
1114 
1115  // At this point we are now comitted to returning an OK
1116  // status unless some DB query error or other exception comes up.
1117  // This way callers don't have to call rollback() if $status is bad
1118  // unless they actually try to catch exceptions (which is rare).
1119  $newRevisionRecord->setPageId( $newid );
1120 
1121  // Save the revision text...
1122  $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
1123  $newLegacyRevision = new Revision( $newRevisionRecord );
1124 
1125  // Update the page record with revision data
1126  // TODO: move to storage service
1127  if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, 0 ) ) {
1128  throw new PageUpdateException( "Failed to update page row to use new revision." );
1129  }
1130 
1131  // TODO: replace legacy hook!
1132  $tags = $this->computeEffectiveTags( $flags );
1133  Hooks::run(
1134  'NewRevisionFromEditComplete',
1135  [ $wikiPage, $newLegacyRevision, false, $user, &$tags ]
1136  );
1137 
1138  // Update recentchanges
1139  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1140  // Add RC row to the DB
1142  $now,
1143  $this->getTitle(),
1144  $newRevisionRecord->isMinor(),
1145  $user,
1146  $summary->text, // TODO: pass object when that becomes possible
1147  ( $flags & EDIT_FORCE_BOT ) > 0,
1148  '',
1149  $newRevisionRecord->getSize(),
1150  $newRevisionRecord->getId(),
1152  $tags
1153  );
1154  }
1155 
1156  $user->incEditCount();
1157 
1158  if ( $this->usePageCreationLog ) {
1159  // Log the page creation
1160  // @TODO: Do we want a 'recreate' action?
1161  $logEntry = new ManualLogEntry( 'create', 'create' );
1162  $logEntry->setPerformer( $user );
1163  $logEntry->setTarget( $this->getTitle() );
1164  $logEntry->setComment( $summary->text );
1165  $logEntry->setTimestamp( $now );
1166  $logEntry->setAssociatedRevId( $newRevisionRecord->getId() );
1167  $logEntry->insert();
1168  // Note that we don't publish page creation events to recentchanges
1169  // (i.e. $logEntry->publish()) since this would create duplicate entries,
1170  // one for the edit and one for the page creation.
1171  }
1172 
1173  $dbw->endAtomic( __METHOD__ );
1174 
1175  // Return the new revision to the caller
1176  // TODO: globally replace usages of 'revision' with getNewRevision()
1177  $status->value['revision'] = $newLegacyRevision;
1178  $status->value['revision-record'] = $newRevisionRecord;
1179 
1180  // Do secondary updates once the main changes have been committed...
1182  $this->getAtomicSectionUpdate(
1183  $dbw,
1184  $wikiPage,
1185  $newRevisionRecord,
1186  $user,
1187  $summary,
1188  $flags,
1189  $status,
1190  [ 'created' => true ]
1191  ),
1193  );
1194 
1195  return $status;
1196  }
1197 
1198  private function getAtomicSectionUpdate(
1199  IDatabase $dbw,
1201  RevisionRecord $newRevisionRecord,
1202  User $user,
1203  CommentStoreComment $summary,
1204  $flags,
1205  Status $status,
1206  $hints = []
1207  ) {
1208  return new AtomicSectionUpdate(
1209  $dbw,
1210  __METHOD__,
1211  function () use (
1212  $wikiPage, $newRevisionRecord, $user,
1213  $summary, $flags, $status, $hints
1214  ) {
1215  // set debug data
1216  $hints['causeAction'] = 'edit-page';
1217  $hints['causeAgent'] = $user->getName();
1218 
1219  $newLegacyRevision = new Revision( $newRevisionRecord );
1220  $mainContent = $newRevisionRecord->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
1221 
1222  // Update links tables, site stats, etc.
1223  $this->derivedDataUpdater->prepareUpdate( $newRevisionRecord, $hints );
1224  $this->derivedDataUpdater->doUpdates();
1225 
1226  // TODO: replace legacy hook!
1227  // TODO: avoid pass-by-reference, see T193950
1228 
1229  if ( $hints['created'] ?? false ) {
1230  // Trigger post-create hook
1231  $params = [ &$wikiPage, &$user, $mainContent, $summary->text,
1232  $flags & EDIT_MINOR, null, null, &$flags, $newLegacyRevision ];
1233  Hooks::run( 'PageContentInsertComplete', $params );
1234  }
1235 
1236  // Trigger post-save hook
1237  $params = [ &$wikiPage, &$user, $mainContent, $summary->text,
1238  $flags & EDIT_MINOR, null, null, &$flags, $newLegacyRevision,
1240  Hooks::run( 'PageContentSaveComplete', $params );
1241  }
1242  );
1243  }
1244 
1248  private function getRequiredSlotRoles() {
1249  return $this->slotRoleRegistry->getRequiredRoles( $this->getTitle() );
1250  }
1251 
1255  private function getAllowedSlotRoles() {
1256  return $this->slotRoleRegistry->getAllowedRoles( $this->getTitle() );
1257  }
1258 
1259  private function ensureRoleAllowed( $role ) {
1260  $allowedRoles = $this->getAllowedSlotRoles();
1261  if ( !in_array( $role, $allowedRoles ) ) {
1262  throw new PageUpdateException( "Slot role `$role` is not allowed." );
1263  }
1264  }
1265 
1266  private function ensureRoleNotRequired( $role ) {
1267  $requiredRoles = $this->getRequiredSlotRoles();
1268  if ( in_array( $role, $requiredRoles ) ) {
1269  throw new PageUpdateException( "Slot role `$role` is required." );
1270  }
1271  }
1272 
1273  private function checkAllRolesAllowed( array $roles, Status $status ) {
1274  $allowedRoles = $this->getAllowedSlotRoles();
1275 
1276  $forbidden = array_diff( $roles, $allowedRoles );
1277  if ( !empty( $forbidden ) ) {
1278  $status->error(
1279  'edit-slots-cannot-add',
1280  count( $forbidden ),
1281  implode( ', ', $forbidden )
1282  );
1283  }
1284  }
1285 
1286  private function checkNoRolesRequired( array $roles, Status $status ) {
1287  $requiredRoles = $this->getRequiredSlotRoles();
1288 
1289  $needed = array_diff( $roles, $requiredRoles );
1290  if ( !empty( $needed ) ) {
1291  $status->error(
1292  'edit-slots-cannot-remove',
1293  count( $needed ),
1294  implode( ', ', $needed )
1295  );
1296  }
1297  }
1298 
1299  private function checkAllRequiredRoles( array $roles, Status $status ) {
1300  $requiredRoles = $this->getRequiredSlotRoles();
1301 
1302  $missing = array_diff( $requiredRoles, $roles );
1303  if ( !empty( $missing ) ) {
1304  $status->error(
1305  'edit-slots-missing',
1306  count( $missing ),
1307  implode( ', ', $missing )
1308  );
1309  }
1310  }
1311 
1312 }
DerivedPageDataUpdater $derivedDataUpdater
Definition: PageUpdater.php:88
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
RevisionSlotsUpdate $slotsUpdate
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.
int $rcPatrolStatus
the RC patrol status the new revision should be marked with.
getOriginalRevisionId()
Returns the ID of an earlier revision that is being repeated or restored by this update.
error( $message,... $parameters)
Add an error, do not set fatal flag This can be used for non-fatal errors.
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
SlotRoleRegistry $slotRoleRegistry
isNew()
Whether saveRevision() was called and created a new page.
wasSuccessful()
Whether saveRevision() completed successfully.
const EDIT_INTERNAL
Definition: Defines.php:139
insertOn( $dbw, $pageId=null)
Insert a new empty page record for this article.
Definition: WikiPage.php:1339
getNewRevision()
The new revision created by saveRevision(), or null if saveRevision() has not yet been called...
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page...
saveRevision(CommentStoreComment $summary, $flags=0)
Change an existing article or create a new article.
checkFlags( $flags)
Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
doModify(CommentStoreComment $summary, User $user, $flags)
setSlot(SlotRecord $slot)
Set the new slot for the given slot role.
Value object representing a modification of revision slots.
const EDIT_MINOR
Definition: Defines.php:134
setContent( $role, Content $content)
Set the new content for the given slot role.
warning( $message,... $parameters)
Add a new warning.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
const EDIT_UPDATE
Definition: Defines.php:133
checkNoRolesRequired(array $roles, Status $status)
removeSlot( $role)
Removes the slot with the given role.
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
addTags(array $tags)
Sets tags to apply to this update.
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
boolean $ajaxEditStash
see $wgAjaxEditStash
const DB_MASTER
Definition: defines.php:26
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...
boolean $useAutomaticEditSummaries
see $wgUseAutomaticEditSummaries
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2283
Exception representing a failure to update a page entry.
checkAllRolesAllowed(array $roles, Status $status)
addTag( $tag)
Sets a tag to apply to this update.
__construct(User $user, WikiPage $wikiPage, DerivedPageDataUpdater $derivedDataUpdater, ILoadBalancer $loadBalancer, RevisionStore $revisionStore, SlotRoleRegistry $slotRoleRegistry)
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
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
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
inheritSlot(SlotRecord $originalSlot)
Explicitly inherit a slot from some earlier revision.
setOriginalRevisionId( $originalRevId)
Sets the ID of an earlier revision that is being repeated or restored by this update.
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
const EDIT_FORCE_BOT
Definition: Defines.php:136
Service for looking up page revisions.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
doCreate(CommentStoreComment $summary, User $user, $flags)
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
incEditCount()
Schedule a deferred update to update the user&#39;s edit count.
Definition: User.php:5031
const EDIT_AUTOSUMMARY
Definition: Defines.php:138
const EDIT_SUPPRESS_RC
Definition: Defines.php:135
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:63
wasCommitted()
Whether saveRevision() has been called on this instance.
getStatus()
The Status object indicating whether saveRevision() was successful, or null if saveRevision() was not...
setRcPatrolStatus( $status)
Sets the "patrolled" status of the edit.
isOK()
Returns whether the operation completed.
grabParentRevision()
Returns the revision that was the page&#39;s current revision when grabParentRevision() was first called...
merge( $other, $overwriteValue=false)
Merge another status object into this one.
const PRC_UNPATROLLED
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:489
A handle for managing updates for derived page data on edit, import, purge, etc.
checkAllRequiredRoles(array $roles, Status $status)
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Database cluster connection, tracking, load balancing, and transaction manager interface.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
getAtomicSectionUpdate(IDatabase $dbw, WikiPage $wikiPage, RevisionRecord $newRevisionRecord, User $user, CommentStoreComment $summary, $flags, Status $status, $hints=[])
const EDIT_NEW
Definition: Defines.php:132
Controller-like object for creating and updating pages by creating new revisions. ...
Definition: PageUpdater.php:73
Exception representing a failure to look up a revision.
isUnchanged()
Whether saveRevision() did not create a revision because the content didn&#39;t change (null-edit)...
setUseAutomaticEditSummaries( $useAutomaticEditSummaries)
Can be used to enable or disable automatic summaries that are applied to certain kinds of changes...
updateRevisionOn( $dbw, $revision, $lastRevision=null, $lastRevIsRedirect=null)
Update the page record to point to a newly saved revision.
Definition: WikiPage.php:1384
setAjaxEditStash( $ajaxEditStash)
Page revision base class.
$content
Definition: router.php:78
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2987
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
makeNewRevision(CommentStoreComment $comment, User $user, $flags, Status $status)
Constructs a MutableRevisionRecord based on the Content prepared by the DerivedPageDataUpdater.
getExplicitTags()
Returns the list of tags set using the addTag() method.
setUndidRevisionId( $undidRevId)
Sets the ID of revision that was undone by the present update.
bool $usePageCreationLog
whether to create a log entry for new page creations.
hasEditConflict( $expectedParentRevision)
Checks whether this update conflicts with another update performed between the client loading data to...
setUsePageCreationLog( $use)
Whether to create a log entry for new page creations.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
getParentContent( $role)
Returns the content of the given slot of the parent revision, with no audience checks applied...
getUndidRevisionId()
Returns the revision ID set by setUndidRevisionId(), indicating what revision is being undone by this...