MediaWiki  master
DerivedPageDataUpdater.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Storage;
24 
26 use Content;
27 use ContentHandler;
29 use DeferredUpdates;
30 use IDBAccessObject;
31 use InvalidArgumentException;
32 use JobQueueGroup;
33 use Language;
34 use LinksUpdate;
35 use LogicException;
53 use MessageCache;
54 use MWTimestamp;
56 use ParserCache;
57 use ParserOptions;
58 use ParserOutput;
59 use Psr\Log\LoggerAwareInterface;
60 use Psr\Log\LoggerInterface;
61 use Psr\Log\NullLogger;
65 use SearchUpdate;
66 use SiteStatsUpdate;
67 use Title;
68 use User;
69 use WANObjectCache;
70 use Wikimedia\Assert\Assert;
72 use WikiPage;
73 
105 class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
106 
110  private $user = null;
111 
115  private $wikiPage;
116 
120  private $parserCache;
121 
125  private $revisionStore;
126 
130  private $contLang;
131 
135  private $jobQueueGroup;
136 
140  private $messageCache;
141 
146 
150  private $hookRunner;
151 
155  private $logger;
156 
161 
165  private $rcWatchCategoryMembership = false;
166 
174  private $options = [
175  'changed' => true,
176  // newrev is true if prepareUpdate is handling the creation of a new revision,
177  // as opposed to a null edit or a forced update.
178  'newrev' => false,
179  'created' => false,
180  'moved' => false,
181  'restored' => false,
182  'oldrevision' => null,
183  'oldcountable' => null,
184  'oldredirect' => null,
185  'triggeringUser' => null,
186  // causeAction/causeAgent default to 'unknown' but that's handled where it's read,
187  // to make the life of prepareUpdate() callers easier.
188  'causeAction' => null,
189  'causeAgent' => null,
190  'editResult' => null,
191  'approved' => false,
192  ];
193 
213  private $pageState = null;
214 
218  private $slotsUpdate = null;
219 
223  private $parentRevision = null;
224 
228  private $revision = null;
229 
233  private $renderedRevision = null;
234 
239 
242 
251  private $stage = 'new';
252 
263  private const TRANSITIONS = [
264  'new' => [
265  'new' => true,
266  'knows-current' => true,
267  'has-content' => true,
268  'has-revision' => true,
269  ],
270  'knows-current' => [
271  'knows-current' => true,
272  'has-content' => true,
273  'has-revision' => true,
274  ],
275  'has-content' => [
276  'has-content' => true,
277  'has-revision' => true,
278  ],
279  'has-revision' => [
280  'has-revision' => true,
281  'done' => true,
282  ],
283  ];
284 
287 
290 
292  private $userNameUtils;
293 
296 
298  private $pageEditStash;
299 
302 
305 
308 
329  public function __construct(
340  HookContainer $hookContainer,
348  ) {
349  $this->wikiPage = $wikiPage;
350 
351  $this->parserCache = $parserCache;
352  $this->revisionStore = $revisionStore;
353  $this->revisionRenderer = $revisionRenderer;
354  $this->slotRoleRegistry = $slotRoleRegistry;
355  $this->jobQueueGroup = $jobQueueGroup;
356  $this->messageCache = $messageCache;
357  $this->contLang = $contLang;
358  // XXX only needed for waiting for replicas to catch up; there should be a narrower
359  // interface for that.
360  $this->loadbalancerFactory = $loadbalancerFactory;
361  $this->contentHandlerFactory = $contentHandlerFactory;
362  $this->hookRunner = new HookRunner( $hookContainer );
363  $this->editResultCache = $editResultCache;
364  $this->userNameUtils = $userNameUtils;
365  $this->contentTransformer = $contentTransformer;
366  $this->pageEditStash = $pageEditStash;
367  $this->talkPageNotificationManager = $talkPageNotificationManager;
368  $this->mainWANObjectCache = $mainWANObjectCache;
369  $this->permissionManager = $permissionManager;
370 
371  $this->logger = new NullLogger();
372  }
373 
379  private static function toLegacyUser( UserIdentity $user ) {
380  return User::newFromIdentity( $user );
381  }
382 
383  public function setLogger( LoggerInterface $logger ) {
384  $this->logger = $logger;
385  }
386 
398  private function doTransition( $newStage ) {
399  $this->assertTransition( $newStage );
400 
401  $oldStage = $this->stage;
402  $this->stage = $newStage;
403 
404  return $oldStage;
405  }
406 
416  private function assertTransition( $newStage ) {
417  if ( empty( self::TRANSITIONS[$this->stage][$newStage] ) ) {
418  throw new LogicException( "Cannot transition from {$this->stage} to $newStage" );
419  }
420  }
421 
433  public function isReusableFor(
434  UserIdentity $user = null,
435  RevisionRecord $revision = null,
437  $parentId = null
438  ) {
439  if ( $revision
440  && $parentId
441  && $revision->getParentId() !== $parentId
442  ) {
443  throw new InvalidArgumentException( '$parentId should match the parent of $revision' );
444  }
445 
446  // NOTE: For null revisions, $user may be different from $this->revision->getUser
447  // and also from $revision->getUser.
448  // But $user should always match $this->user.
449  if ( $user && $this->user && $user->getName() !== $this->user->getName() ) {
450  return false;
451  }
452 
453  if ( $revision && $this->revision && $this->revision->getId()
454  && $this->revision->getId() !== $revision->getId()
455  ) {
456  return false;
457  }
458 
459  if ( $this->pageState
460  && $revision
461  && $revision->getParentId() !== null
462  && $this->pageState['oldId'] !== $revision->getParentId()
463  ) {
464  return false;
465  }
466 
467  if ( $this->pageState
468  && $parentId !== null
469  && $this->pageState['oldId'] !== $parentId
470  ) {
471  return false;
472  }
473 
474  // NOTE: this check is the primary reason for having the $this->slotsUpdate field!
475  if ( $this->slotsUpdate
476  && $slotsUpdate
477  && !$this->slotsUpdate->hasSameUpdates( $slotsUpdate )
478  ) {
479  return false;
480  }
481 
482  if ( $revision
483  && $this->revision
484  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
485  ) {
486  return false;
487  }
488 
489  return true;
490  }
491 
497  $this->articleCountMethod = $articleCountMethod;
498  }
499 
505  $this->rcWatchCategoryMembership = $rcWatchCategoryMembership;
506  }
507 
511  private function getTitle() {
512  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
513  return $this->wikiPage->getTitle();
514  }
515 
519  private function getWikiPage() {
520  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
521  return $this->wikiPage;
522  }
523 
531  public function pageExisted() {
532  $this->assertHasPageState( __METHOD__ );
533 
534  return $this->pageState['oldId'] > 0;
535  }
536 
546  private function getParentRevision() {
547  $this->assertPrepared( __METHOD__ );
548 
549  if ( $this->parentRevision ) {
550  return $this->parentRevision;
551  }
552 
553  if ( !$this->pageState['oldId'] ) {
554  // If there was no current revision, there is no parent revision,
555  // since the page didn't exist.
556  return null;
557  }
558 
559  $oldId = $this->revision->getParentId();
560  $flags = $this->usePrimary() ? RevisionStore::READ_LATEST : 0;
561  $this->parentRevision = $oldId
562  ? $this->revisionStore->getRevisionById( $oldId, $flags )
563  : null;
564 
565  return $this->parentRevision;
566  }
567 
588  public function grabCurrentRevision() {
589  if ( $this->pageState ) {
590  return $this->pageState['oldRevision'];
591  }
592 
593  $this->assertTransition( 'knows-current' );
594 
595  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
596  $wikiPage = $this->getWikiPage();
597 
598  // Do not call WikiPage::clear(), since the caller may already have caused page data
599  // to be loaded with SELECT FOR UPDATE. Just assert it's loaded now.
600  $wikiPage->loadPageData( self::READ_LATEST );
601  $current = $wikiPage->getRevisionRecord();
602 
603  $this->pageState = [
604  'oldRevision' => $current,
605  'oldId' => $current ? $current->getId() : 0,
606  'oldIsRedirect' => $wikiPage->isRedirect(), // NOTE: uses page table
607  'oldCountable' => $wikiPage->isCountable(), // NOTE: uses pagelinks table
608  ];
609 
610  $this->doTransition( 'knows-current' );
611 
612  return $this->pageState['oldRevision'];
613  }
614 
620  public function isContentPrepared() {
621  return $this->revision !== null;
622  }
623 
631  public function isUpdatePrepared() {
632  return $this->revision !== null && $this->revision->getId() !== null;
633  }
634 
638  private function getPageId() {
639  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
640  return $this->wikiPage->getId();
641  }
642 
648  public function isContentDeleted() {
649  if ( $this->revision ) {
650  return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
651  } else {
652  // If the content has not been saved yet, it cannot have been deleted yet.
653  return false;
654  }
655  }
656 
666  public function getRawSlot( $role ) {
667  return $this->getSlots()->getSlot( $role );
668  }
669 
678  public function getRawContent( $role ) {
679  return $this->getRawSlot( $role )->getContent();
680  }
681 
688  private function getContentModel( $role ) {
689  return $this->getRawSlot( $role )->getModel();
690  }
691 
697  private function getContentHandler( $role ): ContentHandler {
698  return $this->contentHandlerFactory
699  ->getContentHandler( $this->getContentModel( $role ) );
700  }
701 
702  private function usePrimary() {
703  // TODO: can we just set a flag to true in prepareContent()?
704  return $this->wikiPage->wasLoadedFrom( self::READ_LATEST );
705  }
706 
710  public function isCountable() {
711  // NOTE: Keep in sync with WikiPage::isCountable.
712 
713  if ( !$this->getTitle()->isContentPage() ) {
714  return false;
715  }
716 
717  if ( $this->isContentDeleted() ) {
718  // This should be irrelevant: countability only applies to the current revision,
719  // and the current revision is never suppressed.
720  return false;
721  }
722 
723  if ( $this->isRedirect() ) {
724  return false;
725  }
726 
727  $hasLinks = null;
728 
729  if ( $this->articleCountMethod === 'link' ) {
730  // NOTE: it would be more appropriate to determine for each slot separately
731  // whether it has links, and use that information with that slot's
732  // isCountable() method. However, that would break parity with
733  // WikiPage::isCountable, which uses the pagelinks table to determine
734  // whether the current revision has links.
735  $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() );
736  }
737 
738  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
739  $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
740  if ( $roleHandler->supportsArticleCount() ) {
741  $content = $this->getRawContent( $role );
742 
743  if ( $content->isCountable( $hasLinks ) ) {
744  return true;
745  }
746  }
747  }
748 
749  return false;
750  }
751 
755  public function isRedirect() {
756  // NOTE: main slot determines redirect status
757  // TODO: MCR: this should be controlled by a PageTypeHandler
758  $mainContent = $this->getRawContent( SlotRecord::MAIN );
759 
760  return $mainContent->isRedirect();
761  }
762 
768  private function revisionIsRedirect( RevisionRecord $rev ) {
769  // NOTE: main slot determines redirect status
770  $mainContent = $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
771 
772  return $mainContent->isRedirect();
773  }
774 
799  public function prepareContent(
802  $useStash = true
803  ) {
804  if ( $this->slotsUpdate ) {
805  if ( !$this->user ) {
806  throw new LogicException(
807  'Unexpected state: $this->slotsUpdate was initialized, '
808  . 'but $this->user was not.'
809  );
810  }
811 
812  if ( $this->user->getName() !== $user->getName() ) {
813  throw new LogicException( 'Can\'t call prepareContent() again for different user! '
814  . 'Expected ' . $this->user->getName() . ', got ' . $user->getName()
815  );
816  }
817 
818  if ( !$this->slotsUpdate->hasSameUpdates( $slotsUpdate ) ) {
819  throw new LogicException(
820  'Can\'t call prepareContent() again with different slot content!'
821  );
822  }
823 
824  return; // prepareContent() already done, nothing to do
825  }
826 
827  $this->assertTransition( 'has-content' );
828 
829  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
830  $title = $this->getTitle();
831 
833 
834  // The edit may have already been prepared via api.php?action=stashedit
835  $stashedEdit = false;
836 
837  $legacyUser = self::toLegacyUser( $user );
838 
839  // TODO: MCR: allow output for all slots to be stashed.
840  if ( $useStash && $slotsUpdate->isModifiedSlot( SlotRecord::MAIN ) ) {
841  $stashedEdit = $this->pageEditStash->checkCache(
842  $title,
844  $legacyUser
845  );
846  }
847 
848  $userPopts = ParserOptions::newFromUserAndLang( $user, $this->contLang );
849  $this->hookRunner->onArticlePrepareTextForEdit( $wikiPage, $userPopts );
850 
851  $this->user = $user;
852  $this->slotsUpdate = $slotsUpdate;
853 
854  if ( $parentRevision ) {
856  } else {
857  $this->revision = new MutableRevisionRecord( $title );
858  }
859 
860  // NOTE: user and timestamp must be set, so they can be used for
861  // {{subst:REVISIONUSER}} and {{subst:REVISIONTIMESTAMP}} in PST!
862  $this->revision->setTimestamp( MWTimestamp::now( TS_MW ) );
863  $this->revision->setUser( $user );
864 
865  // Set up ParserOptions to operate on the new revision
866  $oldCallback = $userPopts->getCurrentRevisionRecordCallback();
867  $userPopts->setCurrentRevisionRecordCallback(
868  function ( Title $parserTitle, $parser = null ) use ( $title, $oldCallback ) {
869  if ( $parserTitle->equals( $title ) ) {
870  return $this->revision;
871  } else {
872  return call_user_func( $oldCallback, $parserTitle, $parser );
873  }
874  }
875  );
876 
877  $pstContentSlots = $this->revision->getSlots();
878 
879  foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
880  $slot = $slotsUpdate->getModifiedSlot( $role );
881 
882  if ( $slot->isInherited() ) {
883  // No PST for inherited slots! Note that "modified" slots may still be inherited
884  // from an earlier version, e.g. for rollbacks.
885  $pstSlot = $slot;
886  } elseif ( $role === SlotRecord::MAIN && $stashedEdit ) {
887  // TODO: MCR: allow PST content for all slots to be stashed.
888  $pstSlot = SlotRecord::newUnsaved( $role, $stashedEdit->pstContent );
889  } else {
890  $pstContent = $this->contentTransformer->preSaveTransform(
891  $slot->getContent(),
892  $title,
893  $user,
894  $userPopts
895  );
896 
897  $pstSlot = SlotRecord::newUnsaved( $role, $pstContent );
898  }
899 
900  $pstContentSlots->setSlot( $pstSlot );
901  }
902 
903  foreach ( $slotsUpdate->getRemovedRoles() as $role ) {
904  $pstContentSlots->removeSlot( $role );
905  }
906 
907  $this->options['created'] = ( $parentRevision === null );
908  $this->options['changed'] = ( $parentRevision === null
909  || !$pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
910 
911  $this->doTransition( 'has-content' );
912 
913  if ( !$this->options['changed'] ) {
914  // null-edit!
915 
916  // TODO: move this into MutableRevisionRecord
917  // TODO: This needs to behave differently for a forced dummy edit!
918  $this->revision->setId( $parentRevision->getId() );
919  $this->revision->setTimestamp( $parentRevision->getTimestamp() );
920  $this->revision->setPageId( $parentRevision->getPageId() );
921  $this->revision->setParentId( $parentRevision->getParentId() );
922  $this->revision->setUser( $parentRevision->getUser( RevisionRecord::RAW ) );
923  $this->revision->setComment( $parentRevision->getComment( RevisionRecord::RAW ) );
924  $this->revision->setMinorEdit( $parentRevision->isMinor() );
925  $this->revision->setVisibility( $parentRevision->getVisibility() );
926 
927  // prepareUpdate() is redundant for null-edits
928  $this->doTransition( 'has-revision' );
929  } else {
930  $this->parentRevision = $parentRevision;
931  }
932 
933  $renderHints = [ 'use-master' => $this->usePrimary(), 'audience' => RevisionRecord::RAW ];
934 
935  if ( $stashedEdit ) {
937  $output = $stashedEdit->output;
938  // TODO: this should happen when stashing the ParserOutput, not now!
939  $output->setCacheTime( $stashedEdit->timestamp );
940 
941  $renderHints['known-revision-output'] = $output;
942 
943  $this->logger->debug( __METHOD__ . ': using stashed edit output...' );
944  }
945 
946  $renderHints['generate-html'] = $this->shouldGenerateHTMLOnEdit();
947 
948  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
949  // NOTE: the revision is either new or current, so we can bypass audience checks.
950  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
951  $this->revision,
952  null,
953  null,
954  $renderHints
955  );
956  }
957 
973  public function getRevision() {
974  $this->assertPrepared( __METHOD__ );
975  return $this->revision;
976  }
977 
981  public function getRenderedRevision() {
982  $this->assertPrepared( __METHOD__ );
983 
985  }
986 
987  private function assertHasPageState( $method ) {
988  if ( !$this->pageState ) {
989  throw new LogicException(
990  'Must call grabCurrentRevision() or prepareContent() '
991  . 'or prepareUpdate() before calling ' . $method
992  );
993  }
994  }
995 
996  private function assertPrepared( $method ) {
997  if ( !$this->revision ) {
998  throw new LogicException(
999  'Must call prepareContent() or prepareUpdate() before calling ' . $method
1000  );
1001  }
1002  }
1003 
1004  private function assertHasRevision( $method ) {
1005  if ( !$this->revision->getId() ) {
1006  throw new LogicException(
1007  'Must call prepareUpdate() before calling ' . $method
1008  );
1009  }
1010  }
1011 
1017  public function isCreation() {
1018  $this->assertPrepared( __METHOD__ );
1019  return $this->options['created'];
1020  }
1021 
1031  public function isChange() {
1032  $this->assertPrepared( __METHOD__ );
1033  return $this->options['changed'];
1034  }
1035 
1041  public function wasRedirect() {
1042  $this->assertHasPageState( __METHOD__ );
1043 
1044  if ( $this->pageState['oldIsRedirect'] === null ) {
1046  $rev = $this->pageState['oldRevision'];
1047  if ( $rev ) {
1048  $this->pageState['oldIsRedirect'] = $this->revisionIsRedirect( $rev );
1049  } else {
1050  $this->pageState['oldIsRedirect'] = false;
1051  }
1052  }
1053 
1054  return $this->pageState['oldIsRedirect'];
1055  }
1056 
1065  public function getSlots() {
1066  $this->assertPrepared( __METHOD__ );
1067  return $this->revision->getSlots();
1068  }
1069 
1075  private function getRevisionSlotsUpdate() {
1076  $this->assertPrepared( __METHOD__ );
1077 
1078  if ( !$this->slotsUpdate ) {
1079  $old = $this->getParentRevision();
1080  $this->slotsUpdate = RevisionSlotsUpdate::newFromRevisionSlots(
1081  $this->revision->getSlots(),
1082  $old ? $old->getSlots() : null
1083  );
1084  }
1085  return $this->slotsUpdate;
1086  }
1087 
1094  public function getTouchedSlotRoles() {
1095  return $this->getRevisionSlotsUpdate()->getTouchedRoles();
1096  }
1097 
1104  public function getModifiedSlotRoles() {
1105  return $this->getRevisionSlotsUpdate()->getModifiedRoles();
1106  }
1107 
1113  public function getRemovedSlotRoles() {
1114  return $this->getRevisionSlotsUpdate()->getRemovedRoles();
1115  }
1116 
1170  public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
1171  Assert::parameter(
1172  !isset( $options['oldrevision'] )
1173  || $options['oldrevision'] instanceof RevisionRecord,
1174  '$options["oldrevision"]',
1175  'must be a RevisionRecord'
1176  );
1177  Assert::parameter(
1178  !isset( $options['triggeringUser'] )
1179  || $options['triggeringUser'] instanceof UserIdentity,
1180  '$options["triggeringUser"]',
1181  'must be a UserIdentity'
1182  );
1183  Assert::parameter(
1184  !isset( $options['editResult'] )
1185  || $options['editResult'] instanceof EditResult,
1186  '$options["editResult"]',
1187  'must be an EditResult'
1188  );
1189 
1190  if ( !$revision->getId() ) {
1191  throw new InvalidArgumentException(
1192  'Revision must have an ID set for it to be used with prepareUpdate()!'
1193  );
1194  }
1195 
1196  if ( $this->revision && $this->revision->getId() ) {
1197  if ( $this->revision->getId() === $revision->getId() ) {
1198  return; // nothing to do!
1199  } else {
1200  throw new LogicException(
1201  'Trying to re-use DerivedPageDataUpdater with revision '
1202  . $revision->getId()
1203  . ', but it\'s already bound to revision '
1204  . $this->revision->getId()
1205  );
1206  }
1207  }
1208 
1209  if ( $this->revision
1210  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
1211  ) {
1212  throw new LogicException(
1213  'The revision provided has mismatching content!'
1214  );
1215  }
1216 
1217  // Override fields defined in $this->options with values from $options.
1218  $this->options = array_intersect_key( $options, $this->options ) + $this->options;
1219 
1220  if ( $this->revision ) {
1221  $oldId = $this->pageState['oldId'] ?? 0;
1222  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1223  } elseif ( isset( $this->options['oldrevision'] ) ) {
1225  $oldRev = $this->options['oldrevision'];
1226  $oldId = $oldRev->getId();
1227  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1228  } else {
1229  $oldId = $revision->getParentId();
1230  }
1231 
1232  if ( $oldId !== null ) {
1233  // XXX: what if $options['changed'] disagrees?
1234  // MovePage creates a dummy revision with changed = false!
1235  // We may want to explicitly distinguish between "no new revision" (null-edit)
1236  // and "new revision without new content" (dummy revision).
1237 
1238  if ( $oldId === $revision->getParentId() ) {
1239  // NOTE: this may still be a NullRevision!
1240  // New revision!
1241  $this->options['changed'] = true;
1242  } elseif ( $oldId === $revision->getId() ) {
1243  // Null-edit!
1244  $this->options['changed'] = false;
1245  } else {
1246  // This indicates that calling code has given us the wrong RevisionRecord object
1247  throw new LogicException(
1248  'The RevisionRecord mismatches old revision ID: '
1249  . 'Old ID is ' . $oldId
1250  . ', parent ID is ' . $revision->getParentId()
1251  . ', revision ID is ' . $revision->getId()
1252  );
1253  }
1254  }
1255 
1256  // If prepareContent() was used to generate the PST content (which is indicated by
1257  // $this->slotsUpdate being set), and this is not a null-edit, then the given
1258  // revision must have the acting user as the revision author. Otherwise, user
1259  // signatures generated by PST would mismatch the user in the revision record.
1260  if ( $this->user !== null && $this->options['changed'] && $this->slotsUpdate ) {
1261  $user = $revision->getUser();
1262  if ( !$this->user->equals( $user ) ) {
1263  throw new LogicException(
1264  'The RevisionRecord provided has a mismatching actor: expected '
1265  . $this->user->getName()
1266  . ', got '
1267  . $user->getName()
1268  );
1269  }
1270  }
1271 
1272  // If $this->pageState was not yet initialized by grabCurrentRevision or prepareContent,
1273  // emulate the state of the page table before the edit, as good as we can.
1274  if ( !$this->pageState ) {
1275  $this->pageState = [
1276  'oldIsRedirect' => isset( $this->options['oldredirect'] )
1277  && is_bool( $this->options['oldredirect'] )
1278  ? $this->options['oldredirect']
1279  : null,
1280  'oldCountable' => isset( $this->options['oldcountable'] )
1281  && is_bool( $this->options['oldcountable'] )
1282  ? $this->options['oldcountable']
1283  : null,
1284  ];
1285 
1286  if ( $this->options['changed'] ) {
1287  // The edit created a new revision
1288  $this->pageState['oldId'] = $revision->getParentId();
1289 
1290  if ( isset( $this->options['oldrevision'] ) ) {
1291  $rev = $this->options['oldrevision'];
1292  $this->pageState['oldRevision'] = $rev;
1293  }
1294  } else {
1295  // This is a null-edit, so the old revision IS the new revision!
1296  $this->pageState['oldId'] = $revision->getId();
1297  $this->pageState['oldRevision'] = $revision;
1298  }
1299  }
1300 
1301  // "created" is forced here
1302  $this->options['created'] = ( $this->options['created'] ||
1303  ( $this->pageState['oldId'] === 0 ) );
1304 
1305  $this->revision = $revision;
1306 
1307  $this->doTransition( 'has-revision' );
1308 
1309  // NOTE: in case we have a User object, don't override with a UserIdentity.
1310  // We already checked that $revision->getUser() mathces $this->user;
1311  if ( !$this->user ) {
1312  $this->user = $revision->getUser( RevisionRecord::RAW );
1313  }
1314 
1315  // Prune any output that depends on the revision ID.
1316  if ( $this->renderedRevision ) {
1317  $this->renderedRevision->updateRevision( $revision );
1318  } else {
1319  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
1320  // NOTE: the revision is either new or current, so we can bypass audience checks.
1321  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
1322  $this->revision,
1323  null,
1324  null,
1325  [
1326  'use-master' => $this->usePrimary(),
1327  'audience' => RevisionRecord::RAW,
1328  'known-revision-output' => $options['known-revision-output'] ?? null
1329  ]
1330  );
1331 
1332  // XXX: Since we presumably are dealing with the current revision,
1333  // we could try to get the ParserOutput from the parser cache.
1334  }
1335 
1336  // TODO: optionally get ParserOutput from the ParserCache here.
1337  // Move the logic used by RefreshLinksJob here!
1338  }
1339 
1344  public function getPreparedEdit() {
1345  $this->assertPrepared( __METHOD__ );
1346 
1348  $preparedEdit = new PreparedEdit();
1349 
1350  $preparedEdit->popts = $this->getCanonicalParserOptions();
1351  $preparedEdit->parserOutputCallback = [ $this, 'getCanonicalParserOutput' ];
1352  $preparedEdit->pstContent = $this->revision->getContent( SlotRecord::MAIN );
1353  $preparedEdit->newContent =
1355  ? $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent()
1356  : $this->revision->getContent( SlotRecord::MAIN ); // XXX: can we just remove this?
1357  $preparedEdit->oldContent = null; // unused. // XXX: could get this from the parent revision
1358  $preparedEdit->revid = $this->revision ? $this->revision->getId() : null;
1359  $preparedEdit->timestamp = $preparedEdit->output->getCacheTime();
1360  $preparedEdit->format = $preparedEdit->pstContent->getDefaultFormat();
1361 
1362  return $preparedEdit;
1363  }
1364 
1370  public function getSlotParserOutput( $role, $generateHtml = true ) {
1371  return $this->getRenderedRevision()->getSlotParserOutput(
1372  $role,
1373  [ 'generate-html' => $generateHtml ]
1374  );
1375  }
1376 
1382  return $this->getRenderedRevision()->getRevisionParserOutput( [ 'generate-html' => false ] );
1383  }
1384 
1388  public function getCanonicalParserOutput() {
1389  return $this->getRenderedRevision()->getRevisionParserOutput();
1390  }
1391 
1395  public function getCanonicalParserOptions() {
1396  return $this->getRenderedRevision()->getOptions();
1397  }
1398 
1404  public function getSecondaryDataUpdates( $recursive = false ) {
1405  if ( $this->isContentDeleted() ) {
1406  // This shouldn't happen, since the current content is always public,
1407  // and DataUpates are only needed for current content.
1408  return [];
1409  }
1410 
1411  $wikiPage = $this->getWikiPage();
1412  $wikiPage->loadPageData( WikiPage::READ_LATEST );
1413  if ( !$wikiPage->exists() ) {
1414  // page deleted while defering the update
1415  return [];
1416  }
1417 
1418  $title = $wikiPage->getTitle();
1419  $allUpdates = [];
1420  $parserOutput = $this->shouldGenerateHTMLOnEdit() ?
1422 
1423  // Construct a LinksUpdate for the combined canonical output.
1424  $linksUpdate = new LinksUpdate(
1425  $title,
1426  $parserOutput,
1427  $recursive
1428  );
1429 
1430  $allUpdates[] = $linksUpdate;
1431  // NOTE: Run updates for all slots, not just the modified slots! Otherwise,
1432  // info for an inherited slot may end up being removed. This is also needed
1433  // to ensure that purges are effective.
1435 
1436  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
1437  $slot = $this->getRawSlot( $role );
1438  $content = $slot->getContent();
1439  $handler = $content->getContentHandler();
1440 
1441  $updates = $handler->getSecondaryDataUpdates(
1442  $title,
1443  $content,
1444  $role,
1446  );
1447 
1448  $allUpdates = array_merge( $allUpdates, $updates );
1449  }
1450 
1451  // XXX: if a slot was removed by an earlier edit, but deletion updates failed to run at
1452  // that time, we don't know for which slots to run deletion updates when purging a page.
1453  // We'd have to examine the entire history of the page to determine that. Perhaps there
1454  // could be a "try extra hard" mode for that case that would run a DB query to find all
1455  // roles/models ever used on the page. On the other hand, removing slots should be quite
1456  // rare, so perhaps this isn't worth the trouble.
1457 
1458  // TODO: consolidate with similar logic in WikiPage::getDeletionUpdates()
1460  foreach ( $this->getRemovedSlotRoles() as $role ) {
1461  // HACK: we should get the content model of the removed slot from a SlotRoleHandler!
1462  // For now, find the slot in the parent revision - if the slot was removed, it should
1463  // always exist in the parent revision.
1464  $parentSlot = $parentRevision->getSlot( $role, RevisionRecord::RAW );
1465  $content = $parentSlot->getContent();
1466  $handler = $content->getContentHandler();
1467 
1468  $updates = $handler->getDeletionUpdates(
1469  $title,
1470  $role
1471  );
1472 
1473  $allUpdates = array_merge( $allUpdates, $updates );
1474  }
1475 
1476  // TODO: hard deprecate SecondaryDataUpdates in favor of RevisionDataUpdates in 1.33!
1477  $this->hookRunner->onRevisionDataUpdates( $title, $renderedRevision, $allUpdates );
1478 
1479  return $allUpdates;
1480  }
1481 
1486  private function shouldGenerateHTMLOnEdit(): bool {
1487  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
1488  $slot = $this->getRawSlot( $role );
1489  $contentHandler = $this->contentHandlerFactory->getContentHandler( $slot->getModel() );
1490  if ( $contentHandler->generateHTMLOnEdit() ) {
1491  return true;
1492  }
1493  }
1494  return false;
1495  }
1496 
1507  public function doUpdates() {
1508  $this->assertTransition( 'done' );
1509 
1510  // TODO: move logic into a PageEventEmitter service
1511 
1512  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
1513 
1514  if ( $this->shouldGenerateHTMLOnEdit() ) {
1515  $this->triggerParserCacheUpdate();
1516  }
1517 
1518  $this->doSecondaryDataUpdates( [
1519  // T52785 do not update any other pages on a null edit
1520  'recursive' => $this->options['changed'],
1521  // Defer the getCannonicalParserOutput() call made by getSecondaryDataUpdates()
1522  'defer' => DeferredUpdates::POSTSEND
1523  ] );
1524 
1525  // TODO: MCR: check if *any* changed slot supports categories!
1526  if ( $this->rcWatchCategoryMembership
1527  && $this->getContentHandler( SlotRecord::MAIN )->supportsCategories() === true
1528  && ( $this->options['changed'] || $this->options['created'] )
1529  && !$this->options['restored']
1530  ) {
1531  // Note: jobs are pushed after deferred updates, so the job should be able to see
1532  // the recent change entry (also done via deferred updates) and carry over any
1533  // bot/deletion/IP flags, ect.
1534  $this->jobQueueGroup->lazyPush(
1536  $this->getTitle(),
1537  $this->revision->getTimestamp()
1538  )
1539  );
1540  }
1541 
1542  $id = $this->getPageId();
1543  $title = $this->getTitle();
1544  $shortTitle = $title->getDBkey();
1545 
1546  if ( !$title->exists() ) {
1547  wfDebug( __METHOD__ . ": Page doesn't exist any more, bailing out" );
1548 
1549  $this->doTransition( 'done' );
1550  return;
1551  }
1552 
1553  DeferredUpdates::addCallableUpdate( function () {
1554  if (
1555  $this->options['oldcountable'] === 'no-change' ||
1556  ( !$this->options['changed'] && !$this->options['moved'] )
1557  ) {
1558  $good = 0;
1559  } elseif ( $this->options['created'] ) {
1560  $good = (int)$this->isCountable();
1561  } elseif ( $this->options['oldcountable'] !== null ) {
1562  $good = (int)$this->isCountable()
1563  - (int)$this->options['oldcountable'];
1564  } elseif ( isset( $this->pageState['oldCountable'] ) ) {
1565  $good = (int)$this->isCountable()
1566  - (int)$this->pageState['oldCountable'];
1567  } else {
1568  $good = 0;
1569  }
1570  $edits = $this->options['changed'] ? 1 : 0;
1571  $pages = $this->options['created'] ? 1 : 0;
1572 
1574  [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
1575  ) );
1576  } );
1577 
1578  // TODO: make search infrastructure aware of slots!
1579  $mainSlot = $this->revision->getSlot( SlotRecord::MAIN );
1580  if ( !$mainSlot->isInherited() && !$this->isContentDeleted() ) {
1581  DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $mainSlot->getContent() ) );
1582  }
1583 
1584  $legacyUser = self::toLegacyUser( $this->user );
1585 
1586  // If this is another user's talk page, update newtalk.
1587  // Don't do this if $options['changed'] = false (null-edits) nor if
1588  // it's a minor edit and the user making the edit doesn't generate notifications for those.
1589  // TODO: the permission check should be performed by the callers, see T276181.
1590  if ( $this->options['changed']
1591  && $title->getNamespace() === NS_USER_TALK
1592 
1593  // TODO User::getTitleKey is just a string manipulation of the user name,
1594  // duplicate it here and use $this->user (a UserIdentity) instead
1595  && $shortTitle != $legacyUser->getTitleKey()
1596  && !( $this->revision->isMinor() && $this->permissionManager
1597  ->userHasRight( $this->user, 'nominornewtalk' )
1598  )
1599  ) {
1600  $recipient = User::newFromName( $shortTitle, false );
1601  if ( !$recipient ) {
1602  wfDebug( __METHOD__ . ": invalid username" );
1603  } else {
1604  // Allow extensions to prevent user notification
1605  // when a new message is added to their talk page
1606  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1607  if ( $this->hookRunner->onArticleEditUpdateNewTalk( $wikiPage, $recipient ) ) {
1608  $revRecord = $this->revision;
1609  if ( $this->userNameUtils->isIP( $shortTitle ) ) {
1610  // An anonymous user
1611  $this->talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord );
1612  } elseif ( $recipient->isRegistered() ) {
1613  $this->talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord );
1614  } else {
1615  wfDebug( __METHOD__ . ": don't need to notify a nonexistent user" );
1616  }
1617  }
1618  }
1619  }
1620 
1621  if ( $title->getNamespace() === NS_MEDIAWIKI
1622  && $this->getRevisionSlotsUpdate()->isModifiedSlot( SlotRecord::MAIN )
1623  ) {
1624  $mainContent = $this->isContentDeleted() ? null : $this->getRawContent( SlotRecord::MAIN );
1625 
1626  $this->messageCache->updateMessageOverride( $title, $mainContent );
1627  }
1628 
1629  // TODO: move onArticleCreate and onArticle into a PageEventEmitter service
1630  if ( $this->options['created'] ) {
1632  } elseif ( $this->options['changed'] ) { // T52785
1633  WikiPage::onArticleEdit( $title, $this->revision, $this->getTouchedSlotRoles() );
1634  } elseif ( $this->options['restored'] ) {
1635  $this->mainWANObjectCache->touchCheckKey(
1636  "DerivedPageDataUpdater:restore:page:$id"
1637  );
1638  }
1639 
1640  $oldRevisionRecord = $this->getParentRevision();
1641 
1642  // TODO: In the wiring, register a listener for this on the new PageEventEmitter
1644  $title,
1645  $oldRevisionRecord,
1646  $this->revision,
1647  $this->loadbalancerFactory->getLocalDomainID()
1648  );
1649 
1650  // Schedule a deferred update for marking reverted edits if applicable.
1652 
1653  $this->doTransition( 'done' );
1654  }
1655 
1656  private function triggerParserCacheUpdate() {
1657  $userParserOptions = ParserOptions::newFromUser( $this->user );
1658  // Decide whether to save the final canonical parser output based on the fact that
1659  // users are typically redirected to viewing pages right after they edit those pages.
1660  // Due to vary-revision-id, getting/saving that output here might require a reparse.
1661  if ( $userParserOptions->matchesForCacheKey( $this->getCanonicalParserOptions() ) ) {
1662  // Whether getting the final output requires a reparse or not, the user will
1663  // need canonical output anyway, since that is what their parser options use.
1664  // A reparse now at least has the benefit of various warm process caches.
1665  $this->doParserCacheUpdate();
1666  } else {
1667  // If the user does not have canonical parse options, then don't risk another parse
1668  // to make output they cannot use on the page refresh that typically occurs after
1669  // editing. Doing the parser output save post-send will still benefit *other* users.
1670  DeferredUpdates::addCallableUpdate( function () {
1671  $this->doParserCacheUpdate();
1672  } );
1673  }
1674  }
1675 
1682  if ( $this->options['editResult'] === null ) {
1683  return;
1684  }
1685 
1686  $editResult = $this->options['editResult'];
1687  if ( !$editResult->isRevert() ) {
1688  return;
1689  }
1690 
1691  if ( $this->options['approved'] ) {
1692  // Enqueue the job
1693  $this->jobQueueGroup->lazyPush(
1695  $this->revision->getId(),
1696  $this->options['editResult']
1697  )
1698  );
1699  } else {
1700  // Cache EditResult for later use
1701  $this->editResultCache->set(
1702  $this->revision->getId(),
1703  $this->options['editResult']
1704  );
1705  }
1706  }
1707 
1720  public function doSecondaryDataUpdates( array $options = [] ) {
1721  $this->assertHasRevision( __METHOD__ );
1722  $options += [ 'recursive' => false, 'defer' => false ];
1723  $deferValues = [ false, DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND ];
1724  if ( !in_array( $options['defer'], $deferValues, true ) ) {
1725  throw new InvalidArgumentException( 'Invalid value for defer: ' . $options['defer'] );
1726  }
1727 
1728  $triggeringUser = $this->options['triggeringUser'] ?? $this->user;
1729  $causeAction = $this->options['causeAction'] ?? 'unknown';
1730  $causeAgent = $this->options['causeAgent'] ?? 'unknown';
1731 
1732  // Bundle all of the data updates into a single deferred update wrapper so that
1733  // any failure will cause at most one refreshLinks job to be enqueued by
1734  // DeferredUpdates::doUpdates(). This is hard to do when there are many separate
1735  // updates that are not defined as being related.
1736  $update = new RefreshSecondaryDataUpdate(
1737  $this->loadbalancerFactory,
1738  $triggeringUser,
1739  $this->wikiPage,
1740  $this->revision,
1741  $this,
1742  [ 'recursive' => $options['recursive'] ]
1743  );
1744  $update->setCause( $causeAction, $causeAgent );
1745 
1746  if ( $options['defer'] === false ) {
1747  DeferredUpdates::attemptUpdate( $update, $this->loadbalancerFactory );
1748  } else {
1749  DeferredUpdates::addUpdate( $update, $options['defer'] );
1750  }
1751  }
1752 
1753  public function doParserCacheUpdate() {
1754  $this->assertHasRevision( __METHOD__ );
1755 
1756  $wikiPage = $this->getWikiPage(); // TODO: ParserCache should accept a RevisionRecord instead
1757 
1758  // NOTE: this may trigger the first parsing of the new content after an edit (when not
1759  // using pre-generated stashed output).
1760  // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
1761  // to be performed post-send. The client could already follow a HTTP redirect to the
1762  // page view, but would then have to wait for a response until rendering is complete.
1763  $output = $this->getCanonicalParserOutput();
1764 
1765  // Save it to the parser cache. Use the revision timestamp in the case of a
1766  // freshly saved edit, as that matches page_touched and a mismatch would trigger an
1767  // unnecessary reparse.
1768  $timestamp = $this->options['newrev'] ? $this->revision->getTimestamp()
1769  : $output->getCacheTime();
1770  $this->parserCache->save(
1771  $output, $wikiPage, $this->getCanonicalParserOptions(),
1772  $timestamp, $this->revision->getId()
1773  );
1774  }
1775 
1776 }
MediaWiki\Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:64
MediaWiki\Storage\RevisionSlotsUpdate\getModifiedRoles
getModifiedRoles()
Returns a list of modified slot roles, that is, roles modified by calling modifySlot(),...
Definition: RevisionSlotsUpdate.php:138
MediaWiki\Storage\DerivedPageDataUpdater\getCanonicalParserOutput
getCanonicalParserOutput()
Definition: DerivedPageDataUpdater.php:1388
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:45
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:38
MediaWiki\Storage\DerivedPageDataUpdater\shouldGenerateHTMLOnEdit
shouldGenerateHTMLOnEdit()
Definition: DerivedPageDataUpdater.php:1486
ContentHandler
A content handler knows how do deal with a specific type of content on a wiki page.
Definition: ContentHandler.php:60
MediaWiki\Storage\RevisionSlotsUpdate\getModifiedSlot
getModifiedSlot( $role)
Returns the SlotRecord associated with the given role, if the slot with that role was modified (and n...
Definition: RevisionSlotsUpdate.php:218
MediaWiki\Storage\RevisionSlotsUpdate\isModifiedSlot
isModifiedSlot( $role)
Returns whether getModifiedSlot() will return a SlotRecord for the given role.
Definition: RevisionSlotsUpdate.php:238
MediaWiki\Storage\DerivedPageDataUpdater\$contLang
Language $contLang
Definition: DerivedPageDataUpdater.php:130
MediaWiki\Revision\RevisionRecord\getContent
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
Definition: RevisionRecord.php:156
MediaWiki\Revision\RevisionRecord\getUser
getUser( $audience=self::FOR_PUBLIC, Authority $performer=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Definition: RevisionRecord.php:389
MediaWiki\Storage\DerivedPageDataUpdater\$logger
LoggerInterface $logger
Definition: DerivedPageDataUpdater.php:155
WikiPage\onArticleCreate
static onArticleCreate(Title $title)
The onArticle*() functions are supposed to be a kind of hooks which should be called whenever any of ...
Definition: WikiPage.php:2793
MediaWiki\Storage\DerivedPageDataUpdater\triggerParserCacheUpdate
triggerParserCacheUpdate()
Definition: DerivedPageDataUpdater.php:1656
MediaWiki\Storage\DerivedPageDataUpdater\$parserCache
ParserCache $parserCache
Definition: DerivedPageDataUpdater.php:120
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
WikiPage\loadPageData
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:466
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:816
MediaWiki\Storage\DerivedPageDataUpdater\pageExisted
pageExisted()
Determines whether the page being edited already existed.
Definition: DerivedPageDataUpdater.php:531
ParserOutput
Definition: ParserOutput.php:31
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:72
MediaWiki\Storage\DerivedPageDataUpdater\getParentRevision
getParentRevision()
Returns the parent revision of the new revision wrapped by this update.
Definition: DerivedPageDataUpdater.php:546
MediaWiki\Storage\DerivedPageDataUpdater\getRevision
getRevision()
Returns the update's target revision - that is, the revision that will be the current revision after ...
Definition: DerivedPageDataUpdater.php:973
MediaWiki\Storage\DerivedPageDataUpdater\$articleCountMethod
string $articleCountMethod
see $wgArticleCountMethod
Definition: DerivedPageDataUpdater.php:160
MediaWiki\Storage\DerivedPageDataUpdater\getSlotParserOutput
getSlotParserOutput( $role, $generateHtml=true)
Definition: DerivedPageDataUpdater.php:1370
MediaWiki\Storage\DerivedPageDataUpdater\$user
UserIdentity null $user
Definition: DerivedPageDataUpdater.php:110
MediaWiki\Storage\DerivedPageDataUpdater\prepareUpdate
prepareUpdate(RevisionRecord $revision, array $options=[])
Prepare derived data updates targeting the given RevisionRecord.
Definition: DerivedPageDataUpdater.php:1170
MediaWiki\Storage\DerivedPageDataUpdater\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: DerivedPageDataUpdater.php:253
MediaWiki\Storage\DerivedPageDataUpdater\setArticleCountMethod
setArticleCountMethod( $articleCountMethod)
Definition: DerivedPageDataUpdater.php:496
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
MediaWiki\Storage\DerivedPageDataUpdater\getSlots
getSlots()
Returns the slots of the target revision, after PST.
Definition: DerivedPageDataUpdater.php:1065
DeferredUpdates\attemptUpdate
static attemptUpdate(DeferrableUpdate $update, ILBFactory $lbFactory)
Attempt to run an update with the appropriate transaction round state it expects.
Definition: DeferredUpdates.php:488
true
return true
Definition: router.php:90
MediaWiki\Storage\DerivedPageDataUpdater\$parentRevision
RevisionRecord null $parentRevision
Definition: DerivedPageDataUpdater.php:223
MediaWiki\Storage\DerivedPageDataUpdater\setRcWatchCategoryMembership
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
Definition: DerivedPageDataUpdater.php:504
MediaWiki\Storage\DerivedPageDataUpdater\getPreparedEdit
getPreparedEdit()
Definition: DerivedPageDataUpdater.php:1344
ResourceLoaderWikiModule
Abstraction for ResourceLoader modules which pull from wiki pages.
Definition: ResourceLoaderWikiModule.php:56
MediaWiki\Revision\RevisionRecord\getPageId
getPageId( $wikiId=self::LOCAL)
Get the page ID.
Definition: RevisionRecord.php:335
SearchUpdate
Database independent search index updater.
Definition: SearchUpdate.php:36
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
Definition: DeferredUpdates.php:119
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:60
MediaWiki\Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaced Revision::isMinor.
Definition: RevisionRecord.php:426
MediaWiki\Storage\DerivedPageDataUpdater\$revisionRenderer
RevisionRenderer $revisionRenderer
Definition: DerivedPageDataUpdater.php:238
LinksUpdate
Class the manages updates of *_link tables as well as similar extension-managed tables.
Definition: LinksUpdate.php:39
ParserOptions\newFromUserAndLang
static newFromUserAndLang(UserIdentity $user, Language $lang)
Get a ParserOptions object from a given user and language.
Definition: ParserOptions.php:1100
MediaWiki\Storage\DerivedPageDataUpdater\$mainWANObjectCache
WANObjectCache $mainWANObjectCache
Definition: DerivedPageDataUpdater.php:304
MediaWiki\Storage\DerivedPageDataUpdater\getPageId
getPageId()
Definition: DerivedPageDataUpdater.php:638
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:606
MediaWiki\Storage\DerivedPageDataUpdater\$options
$options
Stores (most of) the $options parameter of prepareUpdate().
Definition: DerivedPageDataUpdater.php:174
MediaWiki\Storage\DerivedPageDataUpdater\$renderedRevision
RenderedRevision $renderedRevision
Definition: DerivedPageDataUpdater.php:233
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:683
MediaWiki\Storage\DerivedPageDataUpdater\$slotsUpdate
RevisionSlotsUpdate null $slotsUpdate
Definition: DerivedPageDataUpdater.php:218
RevertedTagUpdateJob\newSpec
static newSpec(int $revertRevisionId, EditResult $editResult)
Returns a JobSpecification for this job.
Definition: RevertedTagUpdateJob.php:48
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
MediaWiki\Storage\DerivedPageDataUpdater\$userNameUtils
UserNameUtils $userNameUtils
Definition: DerivedPageDataUpdater.php:292
MediaWiki\Storage\DerivedPageDataUpdater\isRedirect
isRedirect()
Definition: DerivedPageDataUpdater.php:755
MediaWiki\Storage\DerivedPageDataUpdater\getCanonicalParserOptions
getCanonicalParserOptions()
Definition: DerivedPageDataUpdater.php:1395
MediaWiki\Storage\DerivedPageDataUpdater\$permissionManager
PermissionManager $permissionManager
Definition: DerivedPageDataUpdater.php:307
MediaWiki\Storage\DerivedPageDataUpdater\doUpdates
doUpdates()
Do standard updates after page edit, purge, or import.
Definition: DerivedPageDataUpdater.php:1507
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
MediaWiki\User\TalkPageNotificationManager
Manages user talk page notifications.
Definition: TalkPageNotificationManager.php:35
MediaWiki\Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:53
MediaWiki\Storage\DerivedPageDataUpdater\$revisionStore
RevisionStore $revisionStore
Definition: DerivedPageDataUpdater.php:125
MediaWiki\Storage\DerivedPageDataUpdater\assertHasRevision
assertHasRevision( $method)
Definition: DerivedPageDataUpdater.php:1004
MediaWiki\Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
MediaWiki\Storage\DerivedPageDataUpdater\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: DerivedPageDataUpdater.php:241
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(PageIdentity $page, ?RevisionRecord $old, ?RevisionRecord $new, string $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:560
MediaWiki\Storage\DerivedPageDataUpdater\isReusableFor
isReusableFor(UserIdentity $user=null, RevisionRecord $revision=null, RevisionSlotsUpdate $slotsUpdate=null, $parentId=null)
Checks whether this DerivedPageDataUpdater can be re-used for running updates targeting the given rev...
Definition: DerivedPageDataUpdater.php:433
MediaWiki\Storage\PageEditStash
Class for managing stashed edits used by the page updater classes.
Definition: PageEditStash.php:48
MediaWiki\Storage\DerivedPageDataUpdater\setLogger
setLogger(LoggerInterface $logger)
Definition: DerivedPageDataUpdater.php:383
WikiPage\onArticleEdit
static onArticleEdit(Title $title, RevisionRecord $revRecord=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:2885
MediaWiki\Storage\DerivedPageDataUpdater\toLegacyUser
static toLegacyUser(UserIdentity $user)
Definition: DerivedPageDataUpdater.php:379
MediaWiki\Storage\DerivedPageDataUpdater\getRevisionSlotsUpdate
getRevisionSlotsUpdate()
Returns the RevisionSlotsUpdate for this updater.
Definition: DerivedPageDataUpdater.php:1075
DeferredUpdates
Class for managing the deferral of updates within the scope of a PHP script invocation.
Definition: DeferredUpdates.php:82
MediaWiki\Storage\RevisionSlotsUpdate\hasSameUpdates
hasSameUpdates(RevisionSlotsUpdate $other)
Returns true if $other represents the same update - that is, if all methods defined by RevisionSlotsU...
Definition: RevisionSlotsUpdate.php:265
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:311
MediaWiki\Storage\DerivedPageDataUpdater\getContentHandler
getContentHandler( $role)
Definition: DerivedPageDataUpdater.php:697
WikiPage\exists
exists()
Definition: WikiPage.php:596
MediaWiki\Revision\SlotRecord\newUnsaved
static newUnsaved(string $role, Content $content, bool $derived=false)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:155
MediaWiki\Content\Transform\ContentTransformer
A service to transform content.
Definition: ContentTransformer.php:15
MediaWiki\Storage\DerivedPageDataUpdater\assertPrepared
assertPrepared( $method)
Definition: DerivedPageDataUpdater.php:996
MediaWiki\Storage\DerivedPageDataUpdater\getParserOutputForMetaData
getParserOutputForMetaData()
Definition: DerivedPageDataUpdater.php:1381
MediaWiki\Storage\DerivedPageDataUpdater\getContentModel
getContentModel( $role)
Returns the content model of the given slot.
Definition: DerivedPageDataUpdater.php:688
MediaWiki\User\UserIdentity\getName
getName()
$title
$title
Definition: testCompression.php:38
MediaWiki\Storage\DerivedPageDataUpdater\wasRedirect
wasRedirect()
Whether the page was a redirect before the edit.
Definition: DerivedPageDataUpdater.php:1041
SiteStatsUpdate\factory
static factory(array $deltas)
Definition: SiteStatsUpdate.php:71
MediaWiki\Revision\RevisionRecord\getComment
getComment( $audience=self::FOR_PUBLIC, Authority $performer=null)
Fetch revision comment, if it's available to the specified audience.
Definition: RevisionRecord.php:413
SiteStatsUpdate
Class for handling updates to the site_stats table.
Definition: SiteStatsUpdate.php:27
MediaWiki\Storage\DerivedPageDataUpdater\isCreation
isCreation()
Whether the edit creates the page.
Definition: DerivedPageDataUpdater.php:1017
MediaWiki\Storage\DerivedPageDataUpdater\assertTransition
assertTransition( $newStage)
Asserts that a transition to the given stage is possible, without performing it.
Definition: DerivedPageDataUpdater.php:416
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
WikiPage\isCountable
isCountable( $editInfo=false)
Determine whether a page would be suitable for being counted as an article in the site_stats table ba...
Definition: WikiPage.php:969
MediaWiki\Revision\MutableRevisionRecord\newFromParentRevision
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Definition: MutableRevisionRecord.php:55
MediaWiki\Storage\EditResult
Object for storing information about the effects of an edit.
Definition: EditResult.php:38
MediaWiki\Revision\RevisionRecord\getSlots
getSlots()
Returns the slots defined for this revision.
Definition: RevisionRecord.php:222
CategoryMembershipChangeJob
Job to add recent change entries mentioning category membership changes.
Definition: CategoryMembershipChangeJob.php:42
MediaWiki\Revision\RevisionRecord\getParentId
getParentId( $wikiId=self::LOCAL)
Get parent revision ID (the original previous page revision).
Definition: RevisionRecord.php:297
MediaWiki\Storage\RevisionSlotsUpdate
Value object representing a modification of revision slots.
Definition: RevisionSlotsUpdate.php:36
CategoryMembershipChangeJob\newSpec
static newSpec(PageIdentity $page, $revisionTimestamp)
Definition: CategoryMembershipChangeJob.php:53
MediaWiki\Revision\RevisionRenderer
The RevisionRenderer service provides access to rendered output for revisions.
Definition: RevisionRenderer.php:45
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:53
$content
$content
Definition: router.php:76
MediaWiki\Storage\DerivedPageDataUpdater\__construct
__construct(WikiPage $wikiPage, RevisionStore $revisionStore, RevisionRenderer $revisionRenderer, SlotRoleRegistry $slotRoleRegistry, ParserCache $parserCache, JobQueueGroup $jobQueueGroup, MessageCache $messageCache, Language $contLang, ILBFactory $loadbalancerFactory, IContentHandlerFactory $contentHandlerFactory, HookContainer $hookContainer, EditResultCache $editResultCache, UserNameUtils $userNameUtils, ContentTransformer $contentTransformer, PageEditStash $pageEditStash, TalkPageNotificationManager $talkPageNotificationManager, WANObjectCache $mainWANObjectCache, PermissionManager $permissionManager)
Definition: DerivedPageDataUpdater.php:329
MediaWiki\Storage\DerivedPageDataUpdater\isContentDeleted
isContentDeleted()
Whether the content is deleted and thus not visible to the public.
Definition: DerivedPageDataUpdater.php:648
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
MediaWiki\Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:44
MediaWiki\Storage\DerivedPageDataUpdater\getWikiPage
getWikiPage()
Definition: DerivedPageDataUpdater.php:519
MediaWiki\Storage\EditResultCache
Class allowing easy storage and retrieval of EditResults associated with revisions.
Definition: EditResultCache.php:42
MediaWiki\Storage\DerivedPageDataUpdater\isCountable
isCountable()
Definition: DerivedPageDataUpdater.php:710
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:128
MediaWiki\Storage\DerivedPageDataUpdater\isContentPrepared
isContentPrepared()
Whether prepareUpdate() or prepareContent() have been called on this instance.
Definition: DerivedPageDataUpdater.php:620
MediaWiki\Storage\DerivedPageDataUpdater\$loadbalancerFactory
ILBFactory $loadbalancerFactory
Definition: DerivedPageDataUpdater.php:145
MediaWiki\Storage\DerivedPageDataUpdater\$pageEditStash
PageEditStash $pageEditStash
Definition: DerivedPageDataUpdater.php:298
MediaWiki\Storage\DerivedPageDataUpdater\getRemovedSlotRoles
getRemovedSlotRoles()
Returns the role names of the slots removed by the new revision.
Definition: DerivedPageDataUpdater.php:1113
MediaWiki\Storage\DerivedPageDataUpdater\$pageState
array $pageState
The state of the relevant row in page table before the edit.
Definition: DerivedPageDataUpdater.php:213
MediaWiki\Storage
Definition: BlobAccessException.php:23
MediaWiki\Storage\DerivedPageDataUpdater\$messageCache
MessageCache $messageCache
Definition: DerivedPageDataUpdater.php:140
MediaWiki\Storage\DerivedPageDataUpdater\doTransition
doTransition( $newStage)
Transition function for managing the life cycle of this instances.
Definition: DerivedPageDataUpdater.php:398
MediaWiki\Storage\DerivedPageDataUpdater\$talkPageNotificationManager
TalkPageNotificationManager $talkPageNotificationManager
Definition: DerivedPageDataUpdater.php:301
MediaWiki\Storage\DerivedPageDataUpdater\getModifiedSlotRoles
getModifiedSlotRoles()
Returns the role names of the slots modified by the new revision, not including removed roles.
Definition: DerivedPageDataUpdater.php:1104
Content
Base interface for content objects.
Definition: Content.php:35
MediaWiki\Storage\DerivedPageDataUpdater\$jobQueueGroup
JobQueueGroup $jobQueueGroup
Definition: DerivedPageDataUpdater.php:135
Title\equals
equals(object $other)
Compares with another Title.
Definition: Title.php:3490
Title
Represents a title within MediaWiki.
Definition: Title.php:48
MediaWiki\Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:279
MediaWiki\Storage\RevisionSlotsUpdate\newFromRevisionSlots
static newFromRevisionSlots(RevisionSlots $newSlots, RevisionSlots $parentSlots=null)
Constructs a RevisionSlotsUpdate representing the update that turned $parentSlots into $newSlots.
Definition: RevisionSlotsUpdate.php:58
MediaWiki\Storage\DerivedPageDataUpdater\$contentTransformer
ContentTransformer $contentTransformer
Definition: DerivedPageDataUpdater.php:295
MediaWiki\Storage\DerivedPageDataUpdater\getRawContent
getRawContent( $role)
Returns the content of the given slot, with no audience checks.
Definition: DerivedPageDataUpdater.php:678
MediaWiki\User\UserNameUtils
UserNameUtils service.
Definition: UserNameUtils.php:42
MediaWiki\Storage\RevisionSlotsUpdate\getRemovedRoles
getRemovedRoles()
Returns a list of removed slot roles, that is, roles removed by calling removeSlot(),...
Definition: RevisionSlotsUpdate.php:148
MediaWiki\Storage\DerivedPageDataUpdater\getTitle
getTitle()
Definition: DerivedPageDataUpdater.php:511
MediaWiki\Storage\DerivedPageDataUpdater\getSecondaryDataUpdates
getSecondaryDataUpdates( $recursive=false)
Definition: DerivedPageDataUpdater.php:1404
MediaWiki\Storage\DerivedPageDataUpdater\doParserCacheUpdate
doParserCacheUpdate()
Definition: DerivedPageDataUpdater.php:1753
MediaWiki\Storage\DerivedPageDataUpdater\getRenderedRevision
getRenderedRevision()
Definition: DerivedPageDataUpdater.php:981
MediaWiki\Storage\DerivedPageDataUpdater\$editResultCache
EditResultCache $editResultCache
Definition: DerivedPageDataUpdater.php:289
MediaWiki\Revision\RevisionSlots
Value object representing the set of slots belonging to a revision.
Definition: RevisionSlots.php:41
ParserCache
Cache for ParserOutput objects corresponding to the latest page revisions.
Definition: ParserCache.php:63
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
MediaWiki\Storage\DerivedPageDataUpdater\usePrimary
usePrimary()
Definition: DerivedPageDataUpdater.php:702
MediaWiki\Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
Definition: RevisionRecord.php:459
MediaWiki\Storage\DerivedPageDataUpdater\$revision
RevisionRecord null $revision
Definition: DerivedPageDataUpdater.php:228
MediaWiki\Storage\DerivedPageDataUpdater\getRawSlot
getRawSlot( $role)
Returns the slot, modified or inherited, after PST, with no audience checks applied.
Definition: DerivedPageDataUpdater.php:666
MediaWiki\Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
MWUnknownContentModelException
Exception thrown when an unregistered content model is requested.
Definition: MWUnknownContentModelException.php:11
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
MediaWiki\Edit\PreparedEdit
Represents information returned by WikiPage::prepareContentForEdit()
Definition: PreparedEdit.php:35
MediaWiki\Storage\DerivedPageDataUpdater\doSecondaryDataUpdates
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (e.g.
Definition: DerivedPageDataUpdater.php:1720
MediaWiki\Revision\RenderedRevision
RenderedRevision represents the rendered representation of a revision.
Definition: RenderedRevision.php:43
MediaWiki\Storage\DerivedPageDataUpdater\isChange
isChange()
Whether the edit created, or should create, a new revision (that is, it's not a null-edit).
Definition: DerivedPageDataUpdater.php:1031
MediaWiki\Storage\DerivedPageDataUpdater\assertHasPageState
assertHasPageState( $method)
Definition: DerivedPageDataUpdater.php:987
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:556
DeferrableUpdate
Interface that deferrable updates should implement.
Definition: DeferrableUpdate.php:11
RefreshSecondaryDataUpdate
Update object handling the cleanup of secondary data after a page was edited.
Definition: RefreshSecondaryDataUpdate.php:38
MediaWiki\Storage\DerivedPageDataUpdater\$hookRunner
HookRunner $hookRunner
Definition: DerivedPageDataUpdater.php:150
MediaWiki\Storage\DerivedPageDataUpdater\getTouchedSlotRoles
getTouchedSlotRoles()
Returns the role names of the slots touched by the new revision, including removed roles.
Definition: DerivedPageDataUpdater.php:1094
MediaWiki\Storage\DerivedPageDataUpdater\$stage
string $stage
A stage identifier for managing the life cycle of this instance.
Definition: DerivedPageDataUpdater.php:251
MediaWiki\Storage\DerivedPageDataUpdater\maybeEnqueueRevertedTagUpdateJob
maybeEnqueueRevertedTagUpdateJob()
If the edit was a revert and it is considered "approved", enqueues the RevertedTagUpdateJob for it.
Definition: DerivedPageDataUpdater.php:1681
WikiPage\isRedirect
isRedirect()
Is the page a redirect, according to secondary tracking tables? If this is true, getRedirectTarget() ...
Definition: WikiPage.php:621
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:105
MediaWiki\Storage\DerivedPageDataUpdater\$wikiPage
WikiPage $wikiPage
Definition: DerivedPageDataUpdater.php:115
MessageCache
Cache of messages that are defined by MediaWiki namespace pages or by hooks.
Definition: MessageCache.php:52
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
Definition: DeferredUpdates.php:145
MediaWiki\Storage\DerivedPageDataUpdater\$rcWatchCategoryMembership
bool $rcWatchCategoryMembership
see $wgRCWatchCategoryMembership
Definition: DerivedPageDataUpdater.php:165
MediaWiki\Storage\DerivedPageDataUpdater\revisionIsRedirect
revisionIsRedirect(RevisionRecord $rev)
Definition: DerivedPageDataUpdater.php:768
MediaWiki\Revision\RevisionRecord\hasSameContent
hasSameContent(RevisionRecord $rec)
Definition: RevisionRecord.php:118
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:42
MediaWiki\Revision\RevisionRecord\getVisibility
getVisibility()
Get the deletion bitfield of the revision.
Definition: RevisionRecord.php:448
MediaWiki\Storage\DerivedPageDataUpdater\grabCurrentRevision
grabCurrentRevision()
Returns the revision that was the page's current revision when grabCurrentRevision() was first called...
Definition: DerivedPageDataUpdater.php:588
MediaWiki\Storage\DerivedPageDataUpdater\isUpdatePrepared
isUpdatePrepared()
Whether prepareUpdate() has been called on this instance.
Definition: DerivedPageDataUpdater.php:631
RevertedTagUpdateJob
Job for deferring the execution of RevertedTagUpdate.
Definition: RevertedTagUpdateJob.php:38
MediaWiki\Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:180
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
ParserOptions\newFromUser
static newFromUser( $user)
Get a ParserOptions object from a given user.
Definition: ParserOptions.php:1087
Wikimedia\Rdbms\ILBFactory
An interface for generating database load balancers.
Definition: ILBFactory.php:33
JobQueueGroup
Class to handle enqueueing of background jobs.
Definition: JobQueueGroup.php:32
MediaWiki\Storage\DerivedPageDataUpdater\prepareContent
prepareContent(UserIdentity $user, RevisionSlotsUpdate $slotsUpdate, $useStash=true)
Prepare updates based on an update which has not yet been saved.
Definition: DerivedPageDataUpdater.php:799