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 
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 }
DerivedPageDataUpdater $derivedDataUpdater
Definition: PageUpdater.php:87
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:2229
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:4956
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:58
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:72
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:2964
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...