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;
35 use LinksUpdate;
36 use LogicException;
51 use MessageCache;
52 use MWTimestamp;
54 use ParserCache;
55 use ParserOptions;
56 use ParserOutput;
57 use Psr\Log\LoggerAwareInterface;
58 use Psr\Log\LoggerInterface;
59 use Psr\Log\NullLogger;
64 use Revision;
65 use SearchUpdate;
66 use SiteStatsUpdate;
67 use Title;
68 use User;
69 use Wikimedia\Assert\Assert;
71 use WikiPage;
72 
104 class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
105 
109  private $user = null;
110 
114  private $wikiPage;
115 
119  private $parserCache;
120 
124  private $revisionStore;
125 
129  private $contLang;
130 
134  private $jobQueueGroup;
135 
139  private $messageCache;
140 
145 
149  private $hookRunner;
150 
154  private $logger;
155 
160 
164  private $rcWatchCategoryMembership = false;
165 
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 
305  public function __construct(
316  HookContainer $hookContainer,
318  ) {
319  $this->wikiPage = $wikiPage;
320 
321  $this->parserCache = $parserCache;
322  $this->revisionStore = $revisionStore;
323  $this->revisionRenderer = $revisionRenderer;
324  $this->slotRoleRegistry = $slotRoleRegistry;
325  $this->jobQueueGroup = $jobQueueGroup;
326  $this->messageCache = $messageCache;
327  $this->contLang = $contLang;
328  // XXX only needed for waiting for replicas to catch up; there should be a narrower
329  // interface for that.
330  $this->loadbalancerFactory = $loadbalancerFactory;
331  $this->contentHandlerFactory = $contentHandlerFactory;
332  $this->hookRunner = new HookRunner( $hookContainer );
333  $this->editResultCache = $editResultCache;
334 
335  $this->logger = new NullLogger();
336  }
337 
338  public function setLogger( LoggerInterface $logger ) {
339  $this->logger = $logger;
340  }
341 
353  private function doTransition( $newStage ) {
354  $this->assertTransition( $newStage );
355 
356  $oldStage = $this->stage;
357  $this->stage = $newStage;
358 
359  return $oldStage;
360  }
361 
371  private function assertTransition( $newStage ) {
372  if ( empty( self::TRANSITIONS[$this->stage][$newStage] ) ) {
373  throw new LogicException( "Cannot transition from {$this->stage} to $newStage" );
374  }
375  }
376 
388  public function isReusableFor(
389  UserIdentity $user = null,
390  RevisionRecord $revision = null,
392  $parentId = null
393  ) {
394  if ( $revision
395  && $parentId
396  && $revision->getParentId() !== $parentId
397  ) {
398  throw new InvalidArgumentException( '$parentId should match the parent of $revision' );
399  }
400 
401  // NOTE: For null revisions, $user may be different from $this->revision->getUser
402  // and also from $revision->getUser.
403  // But $user should always match $this->user.
404  if ( $user && $this->user && $user->getName() !== $this->user->getName() ) {
405  return false;
406  }
407 
408  if ( $revision && $this->revision && $this->revision->getId()
409  && $this->revision->getId() !== $revision->getId()
410  ) {
411  return false;
412  }
413 
414  if ( $this->pageState
415  && $revision
416  && $revision->getParentId() !== null
417  && $this->pageState['oldId'] !== $revision->getParentId()
418  ) {
419  return false;
420  }
421 
422  if ( $this->pageState
423  && $parentId !== null
424  && $this->pageState['oldId'] !== $parentId
425  ) {
426  return false;
427  }
428 
429  // NOTE: this check is the primary reason for having the $this->slotsUpdate field!
430  if ( $this->slotsUpdate
431  && $slotsUpdate
432  && !$this->slotsUpdate->hasSameUpdates( $slotsUpdate )
433  ) {
434  return false;
435  }
436 
437  if ( $revision
438  && $this->revision
439  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
440  ) {
441  return false;
442  }
443 
444  return true;
445  }
446 
452  $this->articleCountMethod = $articleCountMethod;
453  }
454 
460  $this->rcWatchCategoryMembership = $rcWatchCategoryMembership;
461  }
462 
466  private function getTitle() {
467  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
468  return $this->wikiPage->getTitle();
469  }
470 
474  private function getWikiPage() {
475  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
476  return $this->wikiPage;
477  }
478 
486  public function pageExisted() {
487  $this->assertHasPageState( __METHOD__ );
488 
489  return $this->pageState['oldId'] > 0;
490  }
491 
501  private function getParentRevision() {
502  $this->assertPrepared( __METHOD__ );
503 
504  if ( $this->parentRevision ) {
505  return $this->parentRevision;
506  }
507 
508  if ( !$this->pageState['oldId'] ) {
509  // If there was no current revision, there is no parent revision,
510  // since the page didn't exist.
511  return null;
512  }
513 
514  $oldId = $this->revision->getParentId();
515  $flags = $this->useMaster() ? RevisionStore::READ_LATEST : 0;
516  $this->parentRevision = $oldId
517  ? $this->revisionStore->getRevisionById( $oldId, $flags )
518  : null;
519 
520  return $this->parentRevision;
521  }
522 
543  public function grabCurrentRevision() {
544  if ( $this->pageState ) {
545  return $this->pageState['oldRevision'];
546  }
547 
548  $this->assertTransition( 'knows-current' );
549 
550  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
551  $wikiPage = $this->getWikiPage();
552 
553  // Do not call WikiPage::clear(), since the caller may already have caused page data
554  // to be loaded with SELECT FOR UPDATE. Just assert it's loaded now.
555  $wikiPage->loadPageData( self::READ_LATEST );
556  $current = $wikiPage->getRevisionRecord();
557 
558  $this->pageState = [
559  'oldRevision' => $current,
560  'oldId' => $current ? $current->getId() : 0,
561  'oldIsRedirect' => $wikiPage->isRedirect(), // NOTE: uses page table
562  'oldCountable' => $wikiPage->isCountable(), // NOTE: uses pagelinks table
563  ];
564 
565  $this->doTransition( 'knows-current' );
566 
567  return $this->pageState['oldRevision'];
568  }
569 
575  public function isContentPrepared() {
576  return $this->revision !== null;
577  }
578 
586  public function isUpdatePrepared() {
587  return $this->revision !== null && $this->revision->getId() !== null;
588  }
589 
593  private function getPageId() {
594  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
595  return $this->wikiPage->getId();
596  }
597 
603  public function isContentDeleted() {
604  if ( $this->revision ) {
605  return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
606  } else {
607  // If the content has not been saved yet, it cannot have been deleted yet.
608  return false;
609  }
610  }
611 
621  public function getRawSlot( $role ) {
622  return $this->getSlots()->getSlot( $role );
623  }
624 
633  public function getRawContent( $role ) {
634  return $this->getRawSlot( $role )->getContent();
635  }
636 
643  private function getContentModel( $role ) {
644  return $this->getRawSlot( $role )->getModel();
645  }
646 
652  private function getContentHandler( $role ): ContentHandler {
653  return $this->contentHandlerFactory
654  ->getContentHandler( $this->getContentModel( $role ) );
655  }
656 
657  private function useMaster() {
658  // TODO: can we just set a flag to true in prepareContent()?
659  return $this->wikiPage->wasLoadedFrom( self::READ_LATEST );
660  }
661 
665  public function isCountable() {
666  // NOTE: Keep in sync with WikiPage::isCountable.
667 
668  if ( !$this->getTitle()->isContentPage() ) {
669  return false;
670  }
671 
672  if ( $this->isContentDeleted() ) {
673  // This should be irrelevant: countability only applies to the current revision,
674  // and the current revision is never suppressed.
675  return false;
676  }
677 
678  if ( $this->isRedirect() ) {
679  return false;
680  }
681 
682  $hasLinks = null;
683 
684  if ( $this->articleCountMethod === 'link' ) {
685  // NOTE: it would be more appropriate to determine for each slot separately
686  // whether it has links, and use that information with that slot's
687  // isCountable() method. However, that would break parity with
688  // WikiPage::isCountable, which uses the pagelinks table to determine
689  // whether the current revision has links.
690  $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() );
691  }
692 
693  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
694  $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
695  if ( $roleHandler->supportsArticleCount() ) {
696  $content = $this->getRawContent( $role );
697 
698  if ( $content->isCountable( $hasLinks ) ) {
699  return true;
700  }
701  }
702  }
703 
704  return false;
705  }
706 
710  public function isRedirect() {
711  // NOTE: main slot determines redirect status
712  // TODO: MCR: this should be controlled by a PageTypeHandler
713  $mainContent = $this->getRawContent( SlotRecord::MAIN );
714 
715  return $mainContent->isRedirect();
716  }
717 
723  private function revisionIsRedirect( RevisionRecord $rev ) {
724  // NOTE: main slot determines redirect status
725  $mainContent = $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
726 
727  return $mainContent->isRedirect();
728  }
729 
754  public function prepareContent(
755  User $user,
757  $useStash = true
758  ) {
759  if ( $this->slotsUpdate ) {
760  if ( !$this->user ) {
761  throw new LogicException(
762  'Unexpected state: $this->slotsUpdate was initialized, '
763  . 'but $this->user was not.'
764  );
765  }
766 
767  if ( $this->user->getName() !== $user->getName() ) {
768  throw new LogicException( 'Can\'t call prepareContent() again for different user! '
769  . 'Expected ' . $this->user->getName() . ', got ' . $user->getName()
770  );
771  }
772 
773  if ( !$this->slotsUpdate->hasSameUpdates( $slotsUpdate ) ) {
774  throw new LogicException(
775  'Can\'t call prepareContent() again with different slot content!'
776  );
777  }
778 
779  return; // prepareContent() already done, nothing to do
780  }
781 
782  $this->assertTransition( 'has-content' );
783 
784  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
785  $title = $this->getTitle();
786 
788 
789  // The edit may have already been prepared via api.php?action=stashedit
790  $stashedEdit = false;
791 
792  // TODO: MCR: allow output for all slots to be stashed.
793  if ( $useStash && $slotsUpdate->isModifiedSlot( SlotRecord::MAIN ) ) {
794  $editStash = MediaWikiServices::getInstance()->getPageEditStash();
795  $stashedEdit = $editStash->checkCache(
796  $title,
799  );
800  }
801 
802  $userPopts = ParserOptions::newFromUserAndLang( $user, $this->contLang );
803  $this->hookRunner->onArticlePrepareTextForEdit( $wikiPage, $userPopts );
804 
805  $this->user = $user;
806  $this->slotsUpdate = $slotsUpdate;
807 
808  if ( $parentRevision ) {
810  } else {
811  $this->revision = new MutableRevisionRecord( $title );
812  }
813 
814  // NOTE: user and timestamp must be set, so they can be used for
815  // {{subst:REVISIONUSER}} and {{subst:REVISIONTIMESTAMP}} in PST!
816  $this->revision->setTimestamp( MWTimestamp::now( TS_MW ) );
817  $this->revision->setUser( $user );
818 
819  // Set up ParserOptions to operate on the new revision
820  $oldCallback = $userPopts->getCurrentRevisionRecordCallback();
821  $userPopts->setCurrentRevisionRecordCallback(
822  function ( Title $parserTitle, $parser = null ) use ( $title, $oldCallback ) {
823  if ( $parserTitle->equals( $title ) ) {
824  return $this->revision;
825  } else {
826  return call_user_func( $oldCallback, $parserTitle, $parser );
827  }
828  }
829  );
830 
831  $pstContentSlots = $this->revision->getSlots();
832 
833  foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
834  $slot = $slotsUpdate->getModifiedSlot( $role );
835 
836  if ( $slot->isInherited() ) {
837  // No PST for inherited slots! Note that "modified" slots may still be inherited
838  // from an earlier version, e.g. for rollbacks.
839  $pstSlot = $slot;
840  } elseif ( $role === SlotRecord::MAIN && $stashedEdit ) {
841  // TODO: MCR: allow PST content for all slots to be stashed.
842  $pstSlot = SlotRecord::newUnsaved( $role, $stashedEdit->pstContent );
843  } else {
844  $content = $slot->getContent();
845  $pstContent = $content->preSaveTransform( $title, $this->user, $userPopts );
846  $pstSlot = SlotRecord::newUnsaved( $role, $pstContent );
847  }
848 
849  $pstContentSlots->setSlot( $pstSlot );
850  }
851 
852  foreach ( $slotsUpdate->getRemovedRoles() as $role ) {
853  $pstContentSlots->removeSlot( $role );
854  }
855 
856  $this->options['created'] = ( $parentRevision === null );
857  $this->options['changed'] = ( $parentRevision === null
858  || !$pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
859 
860  $this->doTransition( 'has-content' );
861 
862  if ( !$this->options['changed'] ) {
863  // null-edit!
864 
865  // TODO: move this into MutableRevisionRecord
866  // TODO: This needs to behave differently for a forced dummy edit!
867  $this->revision->setId( $parentRevision->getId() );
868  $this->revision->setTimestamp( $parentRevision->getTimestamp() );
869  $this->revision->setPageId( $parentRevision->getPageId() );
870  $this->revision->setParentId( $parentRevision->getParentId() );
871  $this->revision->setUser( $parentRevision->getUser( RevisionRecord::RAW ) );
872  $this->revision->setComment( $parentRevision->getComment( RevisionRecord::RAW ) );
873  $this->revision->setMinorEdit( $parentRevision->isMinor() );
874  $this->revision->setVisibility( $parentRevision->getVisibility() );
875 
876  // prepareUpdate() is redundant for null-edits
877  $this->doTransition( 'has-revision' );
878  } else {
879  $this->parentRevision = $parentRevision;
880  }
881 
882  $renderHints = [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ];
883 
884  if ( $stashedEdit ) {
886  $output = $stashedEdit->output;
887  // TODO: this should happen when stashing the ParserOutput, not now!
888  $output->setCacheTime( $stashedEdit->timestamp );
889 
890  $renderHints['known-revision-output'] = $output;
891 
892  $this->logger->debug( __METHOD__ . ': using stashed edit output...' );
893  }
894 
895  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
896  // NOTE: the revision is either new or current, so we can bypass audience checks.
897  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
898  $this->revision,
899  null,
900  null,
901  $renderHints
902  );
903  }
904 
920  public function getRevision() {
921  $this->assertPrepared( __METHOD__ );
922  return $this->revision;
923  }
924 
928  public function getRenderedRevision() {
929  $this->assertPrepared( __METHOD__ );
930 
932  }
933 
934  private function assertHasPageState( $method ) {
935  if ( !$this->pageState ) {
936  throw new LogicException(
937  'Must call grabCurrentRevision() or prepareContent() '
938  . 'or prepareUpdate() before calling ' . $method
939  );
940  }
941  }
942 
943  private function assertPrepared( $method ) {
944  if ( !$this->revision ) {
945  throw new LogicException(
946  'Must call prepareContent() or prepareUpdate() before calling ' . $method
947  );
948  }
949  }
950 
951  private function assertHasRevision( $method ) {
952  if ( !$this->revision->getId() ) {
953  throw new LogicException(
954  'Must call prepareUpdate() before calling ' . $method
955  );
956  }
957  }
958 
964  public function isCreation() {
965  $this->assertPrepared( __METHOD__ );
966  return $this->options['created'];
967  }
968 
978  public function isChange() {
979  $this->assertPrepared( __METHOD__ );
980  return $this->options['changed'];
981  }
982 
988  public function wasRedirect() {
989  $this->assertHasPageState( __METHOD__ );
990 
991  if ( $this->pageState['oldIsRedirect'] === null ) {
993  $rev = $this->pageState['oldRevision'];
994  if ( $rev ) {
995  $this->pageState['oldIsRedirect'] = $this->revisionIsRedirect( $rev );
996  } else {
997  $this->pageState['oldIsRedirect'] = false;
998  }
999  }
1000 
1001  return $this->pageState['oldIsRedirect'];
1002  }
1003 
1012  public function getSlots() {
1013  $this->assertPrepared( __METHOD__ );
1014  return $this->revision->getSlots();
1015  }
1016 
1022  private function getRevisionSlotsUpdate() {
1023  $this->assertPrepared( __METHOD__ );
1024 
1025  if ( !$this->slotsUpdate ) {
1026  $old = $this->getParentRevision();
1027  $this->slotsUpdate = RevisionSlotsUpdate::newFromRevisionSlots(
1028  $this->revision->getSlots(),
1029  $old ? $old->getSlots() : null
1030  );
1031  }
1032  return $this->slotsUpdate;
1033  }
1034 
1041  public function getTouchedSlotRoles() {
1042  return $this->getRevisionSlotsUpdate()->getTouchedRoles();
1043  }
1044 
1051  public function getModifiedSlotRoles() {
1052  return $this->getRevisionSlotsUpdate()->getModifiedRoles();
1053  }
1054 
1060  public function getRemovedSlotRoles() {
1061  return $this->getRevisionSlotsUpdate()->getRemovedRoles();
1062  }
1063 
1118  public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
1119  if ( isset( $options['oldrevision'] ) && $options['oldrevision'] instanceof Revision ) {
1120  wfDeprecated(
1121  __METHOD__ . ' with the `oldrevision` option being a ' .
1122  'Revision object',
1123  '1.35'
1124  );
1125  $options['oldrevision'] = $options['oldrevision']->getRevisionRecord();
1126  }
1127  Assert::parameter(
1128  !isset( $options['oldrevision'] )
1129  || $options['oldrevision'] instanceof RevisionRecord,
1130  '$options["oldrevision"]',
1131  'must be a RevisionRecord (or Revision)'
1132  );
1133  Assert::parameter(
1134  !isset( $options['triggeringUser'] )
1135  || $options['triggeringUser'] instanceof UserIdentity,
1136  '$options["triggeringUser"]',
1137  'must be a UserIdentity'
1138  );
1139  Assert::parameter(
1140  !isset( $options['editResult'] )
1141  || $options['editResult'] instanceof EditResult,
1142  '$options["editResult"]',
1143  'must be an EditResult'
1144  );
1145 
1146  if ( !$revision->getId() ) {
1147  throw new InvalidArgumentException(
1148  'Revision must have an ID set for it to be used with prepareUpdate()!'
1149  );
1150  }
1151 
1152  if ( $this->revision && $this->revision->getId() ) {
1153  if ( $this->revision->getId() === $revision->getId() ) {
1154  return; // nothing to do!
1155  } else {
1156  throw new LogicException(
1157  'Trying to re-use DerivedPageDataUpdater with revision '
1158  . $revision->getId()
1159  . ', but it\'s already bound to revision '
1160  . $this->revision->getId()
1161  );
1162  }
1163  }
1164 
1165  if ( $this->revision
1166  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
1167  ) {
1168  throw new LogicException(
1169  'The Revision provided has mismatching content!'
1170  );
1171  }
1172 
1173  // Override fields defined in $this->options with values from $options.
1174  $this->options = array_intersect_key( $options, $this->options ) + $this->options;
1175 
1176  if ( $this->revision ) {
1177  $oldId = $this->pageState['oldId'] ?? 0;
1178  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1179  } elseif ( isset( $this->options['oldrevision'] ) ) {
1181  $oldRev = $this->options['oldrevision'];
1182  $oldId = $oldRev->getId();
1183  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1184  } else {
1185  $oldId = $revision->getParentId();
1186  }
1187 
1188  if ( $oldId !== null ) {
1189  // XXX: what if $options['changed'] disagrees?
1190  // MovePage creates a dummy revision with changed = false!
1191  // We may want to explicitly distinguish between "no new revision" (null-edit)
1192  // and "new revision without new content" (dummy revision).
1193 
1194  if ( $oldId === $revision->getParentId() ) {
1195  // NOTE: this may still be a NullRevision!
1196  // New revision!
1197  $this->options['changed'] = true;
1198  } elseif ( $oldId === $revision->getId() ) {
1199  // Null-edit!
1200  $this->options['changed'] = false;
1201  } else {
1202  // This indicates that calling code has given us the wrong Revision object
1203  throw new LogicException(
1204  'The Revision mismatches old revision ID: '
1205  . 'Old ID is ' . $oldId
1206  . ', parent ID is ' . $revision->getParentId()
1207  . ', revision ID is ' . $revision->getId()
1208  );
1209  }
1210  }
1211 
1212  // If prepareContent() was used to generate the PST content (which is indicated by
1213  // $this->slotsUpdate being set), and this is not a null-edit, then the given
1214  // revision must have the acting user as the revision author. Otherwise, user
1215  // signatures generated by PST would mismatch the user in the revision record.
1216  if ( $this->user !== null && $this->options['changed'] && $this->slotsUpdate ) {
1217  $user = $revision->getUser();
1218  if ( !$this->user->equals( $user ) ) {
1219  throw new LogicException(
1220  'The Revision provided has a mismatching actor: expected '
1221  . $this->user->getName()
1222  . ', got '
1223  . $user->getName()
1224  );
1225  }
1226  }
1227 
1228  // If $this->pageState was not yet initialized by grabCurrentRevision or prepareContent,
1229  // emulate the state of the page table before the edit, as good as we can.
1230  if ( !$this->pageState ) {
1231  $this->pageState = [
1232  'oldIsRedirect' => isset( $this->options['oldredirect'] )
1233  && is_bool( $this->options['oldredirect'] )
1234  ? $this->options['oldredirect']
1235  : null,
1236  'oldCountable' => isset( $this->options['oldcountable'] )
1237  && is_bool( $this->options['oldcountable'] )
1238  ? $this->options['oldcountable']
1239  : null,
1240  ];
1241 
1242  if ( $this->options['changed'] ) {
1243  // The edit created a new revision
1244  $this->pageState['oldId'] = $revision->getParentId();
1245 
1246  if ( isset( $this->options['oldrevision'] ) ) {
1247  $rev = $this->options['oldrevision'];
1248  $this->pageState['oldRevision'] = $rev;
1249  }
1250  } else {
1251  // This is a null-edit, so the old revision IS the new revision!
1252  $this->pageState['oldId'] = $revision->getId();
1253  $this->pageState['oldRevision'] = $revision;
1254  }
1255  }
1256 
1257  // "created" is forced here
1258  $this->options['created'] = ( $this->options['created'] ||
1259  ( $this->pageState['oldId'] === 0 ) );
1260 
1261  $this->revision = $revision;
1262 
1263  $this->doTransition( 'has-revision' );
1264 
1265  // NOTE: in case we have a User object, don't override with a UserIdentity.
1266  // We already checked that $revision->getUser() mathces $this->user;
1267  if ( !$this->user ) {
1268  $this->user = $revision->getUser( RevisionRecord::RAW );
1269  }
1270 
1271  // Prune any output that depends on the revision ID.
1272  if ( $this->renderedRevision ) {
1273  $this->renderedRevision->updateRevision( $revision );
1274  } else {
1275  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
1276  // NOTE: the revision is either new or current, so we can bypass audience checks.
1277  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
1278  $this->revision,
1279  null,
1280  null,
1281  [
1282  'use-master' => $this->useMaster(),
1283  'audience' => RevisionRecord::RAW,
1284  'known-revision-output' => $options['known-revision-output'] ?? null
1285  ]
1286  );
1287 
1288  // XXX: Since we presumably are dealing with the current revision,
1289  // we could try to get the ParserOutput from the parser cache.
1290  }
1291 
1292  // TODO: optionally get ParserOutput from the ParserCache here.
1293  // Move the logic used by RefreshLinksJob here!
1294  }
1295 
1300  public function getPreparedEdit() {
1301  $this->assertPrepared( __METHOD__ );
1302 
1304  $preparedEdit = new PreparedEdit();
1305 
1306  $preparedEdit->popts = $this->getCanonicalParserOptions();
1307  $preparedEdit->parserOutputCallback = [ $this, 'getCanonicalParserOutput' ];
1308  $preparedEdit->pstContent = $this->revision->getContent( SlotRecord::MAIN );
1309  $preparedEdit->newContent =
1311  ? $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent()
1312  : $this->revision->getContent( SlotRecord::MAIN ); // XXX: can we just remove this?
1313  $preparedEdit->oldContent = null; // unused. // XXX: could get this from the parent revision
1314  $preparedEdit->revid = $this->revision ? $this->revision->getId() : null;
1315  $preparedEdit->timestamp = $preparedEdit->output->getCacheTime();
1316  $preparedEdit->format = $preparedEdit->pstContent->getDefaultFormat();
1317 
1318  return $preparedEdit;
1319  }
1320 
1326  public function getSlotParserOutput( $role, $generateHtml = true ) {
1327  return $this->getRenderedRevision()->getSlotParserOutput(
1328  $role,
1329  [ 'generate-html' => $generateHtml ]
1330  );
1331  }
1332 
1336  public function getCanonicalParserOutput() {
1337  return $this->getRenderedRevision()->getRevisionParserOutput();
1338  }
1339 
1343  public function getCanonicalParserOptions() {
1344  return $this->getRenderedRevision()->getOptions();
1345  }
1346 
1352  public function getSecondaryDataUpdates( $recursive = false ) {
1353  if ( $this->isContentDeleted() ) {
1354  // This shouldn't happen, since the current content is always public,
1355  // and DataUpates are only needed for current content.
1356  return [];
1357  }
1358 
1359  $output = $this->getCanonicalParserOutput();
1360 
1361  // Construct a LinksUpdate for the combined canonical output.
1362  $linksUpdate = new LinksUpdate(
1363  $this->getTitle(),
1364  $output,
1365  $recursive
1366  );
1367 
1368  $allUpdates = [ $linksUpdate ];
1369 
1370  // NOTE: Run updates for all slots, not just the modified slots! Otherwise,
1371  // info for an inherited slot may end up being removed. This is also needed
1372  // to ensure that purges are effective.
1374  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
1375  $slot = $this->getRawSlot( $role );
1376  $content = $slot->getContent();
1377  $handler = $content->getContentHandler();
1378 
1379  $updates = $handler->getSecondaryDataUpdates(
1380  $this->getTitle(),
1381  $content,
1382  $role,
1384  );
1385  $allUpdates = array_merge( $allUpdates, $updates );
1386 
1387  // TODO: remove B/C hack in 1.32!
1388  // NOTE: we assume that the combined output contains all relevant meta-data for
1389  // all slots!
1390  $legacyUpdates = $content->getSecondaryDataUpdates(
1391  $this->getTitle(),
1392  null,
1393  $recursive,
1394  $output
1395  );
1396 
1397  // HACK: filter out redundant and incomplete LinksUpdates
1398  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1399  return !( $update instanceof LinksUpdate );
1400  } );
1401 
1402  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1403  }
1404 
1405  // XXX: if a slot was removed by an earlier edit, but deletion updates failed to run at
1406  // that time, we don't know for which slots to run deletion updates when purging a page.
1407  // We'd have to examine the entire history of the page to determine that. Perhaps there
1408  // could be a "try extra hard" mode for that case that would run a DB query to find all
1409  // roles/models ever used on the page. On the other hand, removing slots should be quite
1410  // rare, so perhaps this isn't worth the trouble.
1411 
1412  // TODO: consolidate with similar logic in WikiPage::getDeletionUpdates()
1413  $wikiPage = $this->getWikiPage();
1415  foreach ( $this->getRemovedSlotRoles() as $role ) {
1416  // HACK: we should get the content model of the removed slot from a SlotRoleHandler!
1417  // For now, find the slot in the parent revision - if the slot was removed, it should
1418  // always exist in the parent revision.
1419  $parentSlot = $parentRevision->getSlot( $role, RevisionRecord::RAW );
1420  $content = $parentSlot->getContent();
1421  $handler = $content->getContentHandler();
1422 
1423  $updates = $handler->getDeletionUpdates(
1424  $this->getTitle(),
1425  $role
1426  );
1427  $allUpdates = array_merge( $allUpdates, $updates );
1428 
1429  // TODO: remove B/C hack in 1.32!
1430  $legacyUpdates = $content->getDeletionUpdates( $wikiPage );
1431 
1432  // HACK: filter out redundant and incomplete LinksDeletionUpdate
1433  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1434  return !( $update instanceof LinksDeletionUpdate );
1435  } );
1436 
1437  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1438  }
1439 
1440  // TODO: hard deprecate SecondaryDataUpdates in favor of RevisionDataUpdates in 1.33!
1441  $this->hookRunner->onRevisionDataUpdates(
1442  $this->getTitle(), $renderedRevision, $allUpdates );
1443 
1444  return $allUpdates;
1445  }
1446 
1457  public function doUpdates() {
1458  $this->assertTransition( 'done' );
1459 
1460  // TODO: move logic into a PageEventEmitter service
1461 
1462  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
1463 
1464  $legacyUser = User::newFromIdentity( $this->user );
1465 
1466  $userParserOptions = ParserOptions::newFromUser( $legacyUser );
1467  // Decide whether to save the final canonical parser ouput based on the fact that
1468  // users are typically redirected to viewing pages right after they edit those pages.
1469  // Due to vary-revision-id, getting/saving that output here might require a reparse.
1470  if ( $userParserOptions->matchesForCacheKey( $this->getCanonicalParserOptions() ) ) {
1471  // Whether getting the final output requires a reparse or not, the user will
1472  // need canonical output anyway, since that is what their parser options use.
1473  // A reparse now at least has the benefit of various warm process caches.
1474  $this->doParserCacheUpdate();
1475  } else {
1476  // If the user does not have canonical parse options, then don't risk another parse
1477  // to make output they cannot use on the page refresh that typically occurs after
1478  // editing. Doing the parser output save post-send will still benefit *other* users.
1479  DeferredUpdates::addCallableUpdate( function () {
1480  $this->doParserCacheUpdate();
1481  } );
1482  }
1483 
1484  $this->doSecondaryDataUpdates( [
1485  // T52785 do not update any other pages on a null edit
1486  'recursive' => $this->options['changed'],
1487  // Defer the getCannonicalParserOutput() call made by getSecondaryDataUpdates()
1488  'defer' => DeferredUpdates::POSTSEND
1489  ] );
1490 
1491  // TODO: MCR: check if *any* changed slot supports categories!
1492  if ( $this->rcWatchCategoryMembership
1493  && $this->getContentHandler( SlotRecord::MAIN )->supportsCategories() === true
1494  && ( $this->options['changed'] || $this->options['created'] )
1495  && !$this->options['restored']
1496  ) {
1497  // Note: jobs are pushed after deferred updates, so the job should be able to see
1498  // the recent change entry (also done via deferred updates) and carry over any
1499  // bot/deletion/IP flags, ect.
1500  $this->jobQueueGroup->lazyPush(
1502  $this->getTitle(),
1503  $this->revision->getTimestamp()
1504  )
1505  );
1506  }
1507 
1508  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1509  // @note: Extensions should *avoid* calling getCannonicalParserOutput() when using
1510  // this hook whenever possible in order to avoid unnecessary additional parses.
1511  $editInfo = $this->getPreparedEdit();
1512  $this->hookRunner->onArticleEditUpdates( $wikiPage, $editInfo, $this->options['changed'] );
1513 
1514  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1515  if ( $this->hookRunner->onArticleEditUpdatesDeleteFromRecentchanges( $wikiPage ) ) {
1516  // Flush old entries from the `recentchanges` table
1517  if ( mt_rand( 0, 9 ) == 0 ) {
1518  $this->jobQueueGroup->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
1519  }
1520  }
1521 
1522  $id = $this->getPageId();
1523  $title = $this->getTitle();
1524  $shortTitle = $title->getDBkey();
1525 
1526  if ( !$title->exists() ) {
1527  wfDebug( __METHOD__ . ": Page doesn't exist any more, bailing out" );
1528 
1529  $this->doTransition( 'done' );
1530  return;
1531  }
1532 
1533  DeferredUpdates::addCallableUpdate( function () {
1534  if (
1535  $this->options['oldcountable'] === 'no-change' ||
1536  ( !$this->options['changed'] && !$this->options['moved'] )
1537  ) {
1538  $good = 0;
1539  } elseif ( $this->options['created'] ) {
1540  $good = (int)$this->isCountable();
1541  } elseif ( $this->options['oldcountable'] !== null ) {
1542  $good = (int)$this->isCountable()
1543  - (int)$this->options['oldcountable'];
1544  } elseif ( isset( $this->pageState['oldCountable'] ) ) {
1545  $good = (int)$this->isCountable()
1546  - (int)$this->pageState['oldCountable'];
1547  } else {
1548  $good = 0;
1549  }
1550  $edits = $this->options['changed'] ? 1 : 0;
1551  $pages = $this->options['created'] ? 1 : 0;
1552 
1554  [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
1555  ) );
1556  } );
1557 
1558  // TODO: make search infrastructure aware of slots!
1559  $mainSlot = $this->revision->getSlot( SlotRecord::MAIN );
1560  if ( !$mainSlot->isInherited() && !$this->isContentDeleted() ) {
1561  DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $mainSlot->getContent() ) );
1562  }
1563 
1564  // If this is another user's talk page, update newtalk.
1565  // Don't do this if $options['changed'] = false (null-edits) nor if
1566  // it's a minor edit and the user making the edit doesn't generate notifications for those.
1567  if ( $this->options['changed']
1568  && $title->getNamespace() === NS_USER_TALK
1569  && $shortTitle != $legacyUser->getTitleKey()
1570  && !( $this->revision->isMinor() && MediaWikiServices::getInstance()
1571  ->getPermissionManager()
1572  ->userHasRight( $legacyUser, 'nominornewtalk' ) )
1573  ) {
1574  $recipient = User::newFromName( $shortTitle, false );
1575  if ( !$recipient ) {
1576  wfDebug( __METHOD__ . ": invalid username" );
1577  } else {
1578  // Allow extensions to prevent user notification
1579  // when a new message is added to their talk page
1580  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1581  if ( $this->hookRunner->onArticleEditUpdateNewTalk( $wikiPage, $recipient ) ) {
1582  $revRecord = $this->revision;
1583  $talkPageNotificationManager = MediaWikiServices::getInstance()
1584  ->getTalkPageNotificationManager();
1585  if ( User::isIP( $shortTitle ) ) {
1586  // An anonymous user
1587  $talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord );
1588  } elseif ( $recipient->isLoggedIn() ) {
1589  $talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord );
1590  } else {
1591  wfDebug( __METHOD__ . ": don't need to notify a nonexistent user" );
1592  }
1593  }
1594  }
1595  }
1596 
1597  if ( $title->getNamespace() === NS_MEDIAWIKI
1598  && $this->getRevisionSlotsUpdate()->isModifiedSlot( SlotRecord::MAIN )
1599  ) {
1600  $mainContent = $this->isContentDeleted() ? null : $this->getRawContent( SlotRecord::MAIN );
1601 
1602  $this->messageCache->updateMessageOverride( $title, $mainContent );
1603  }
1604 
1605  // TODO: move onArticleCreate and onArticle into a PageEventEmitter service
1606  if ( $this->options['created'] ) {
1608  } elseif ( $this->options['changed'] ) { // T52785
1609  WikiPage::onArticleEdit( $title, $this->revision, $this->getTouchedSlotRoles() );
1610  } elseif ( $this->options['restored'] ) {
1611  MediaWikiServices::getInstance()->getMainWANObjectCache()->touchCheckKey(
1612  "DerivedPageDataUpdater:restore:page:$id"
1613  );
1614  }
1615 
1616  $oldRevisionRecord = $this->getParentRevision();
1617 
1618  // TODO: In the wiring, register a listener for this on the new PageEventEmitter
1620  $title,
1621  $oldRevisionRecord,
1622  $this->revision,
1623  $this->loadbalancerFactory->getLocalDomainID()
1624  );
1625 
1626  // Schedule a deferred update for marking reverted edits if applicable.
1628 
1629  $this->doTransition( 'done' );
1630  }
1631 
1638  if ( $this->options['editResult'] === null ) {
1639  return;
1640  }
1641 
1642  $editResult = $this->options['editResult'];
1643  if ( !$editResult->isRevert() ) {
1644  return;
1645  }
1646 
1647  if ( $this->options['approved'] ) {
1648  // Enqueue the job
1649  $this->jobQueueGroup->lazyPush(
1651  $this->revision->getId(),
1652  $this->options['editResult']
1653  )
1654  );
1655  } else {
1656  // Cache EditResult for later use
1657  $this->editResultCache->set(
1658  $this->revision->getId(),
1659  $this->options['editResult']
1660  );
1661  }
1662  }
1663 
1676  public function doSecondaryDataUpdates( array $options = [] ) {
1677  $this->assertHasRevision( __METHOD__ );
1678  $options += [ 'recursive' => false, 'defer' => false ];
1679  $deferValues = [ false, DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND ];
1680  if ( !in_array( $options['defer'], $deferValues, true ) ) {
1681  throw new InvalidArgumentException( 'Invalid value for defer: ' . $options['defer'] );
1682  }
1683 
1684  $triggeringUser = $this->options['triggeringUser'] ?? $this->user;
1685  if ( !$triggeringUser instanceof User ) {
1686  $triggeringUser = User::newFromIdentity( $triggeringUser );
1687  }
1688  $causeAction = $this->options['causeAction'] ?? 'unknown';
1689  $causeAgent = $this->options['causeAgent'] ?? 'unknown';
1690 
1691  // Bundle all of the data updates into a single deferred update wrapper so that
1692  // any failure will cause at most one refreshLinks job to be enqueued by
1693  // DeferredUpdates::doUpdates(). This is hard to do when there are many separate
1694  // updates that are not defined as being related.
1695  $update = new RefreshSecondaryDataUpdate(
1696  $this->loadbalancerFactory,
1697  $triggeringUser,
1698  $this->wikiPage,
1699  $this->revision,
1700  $this,
1701  [ 'recursive' => $options['recursive'] ]
1702  );
1703  $update->setCause( $causeAction, $causeAgent );
1704 
1705  if ( $options['defer'] === false ) {
1706  DeferredUpdates::attemptUpdate( $update, $this->loadbalancerFactory );
1707  } else {
1708  DeferredUpdates::addUpdate( $update, $options['defer'] );
1709  }
1710  }
1711 
1712  public function doParserCacheUpdate() {
1713  $this->assertHasRevision( __METHOD__ );
1714 
1715  $wikiPage = $this->getWikiPage(); // TODO: ParserCache should accept a RevisionRecord instead
1716 
1717  // NOTE: this may trigger the first parsing of the new content after an edit (when not
1718  // using pre-generated stashed output).
1719  // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
1720  // to be performed post-send. The client could already follow a HTTP redirect to the
1721  // page view, but would then have to wait for a response until rendering is complete.
1722  $output = $this->getCanonicalParserOutput();
1723 
1724  // Save it to the parser cache. Use the revision timestamp in the case of a
1725  // freshly saved edit, as that matches page_touched and a mismatch would trigger an
1726  // unnecessary reparse.
1727  $timestamp = $this->options['newrev'] ? $this->revision->getTimestamp()
1728  : $output->getCacheTime();
1729  $this->parserCache->save(
1730  $output, $wikiPage, $this->getCanonicalParserOptions(),
1731  $timestamp, $this->revision->getId()
1732  );
1733  }
1734 
1735 }
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:1336
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:44
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:34
ContentHandler
A content handler knows how do deal with a specific type of content on a wiki page.
Definition: ContentHandler.php:59
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:235
MediaWiki\Storage\DerivedPageDataUpdater\$contLang
Language $contLang
Definition: DerivedPageDataUpdater.php:129
MediaWiki\Storage\DerivedPageDataUpdater\$logger
LoggerInterface $logger
Definition: DerivedPageDataUpdater.php:154
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:3464
MediaWiki\Storage\DerivedPageDataUpdater\$parserCache
ParserCache $parserCache
Definition: DerivedPageDataUpdater.php:119
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
WikiPage\loadPageData
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:429
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:746
MediaWiki\Storage\DerivedPageDataUpdater\pageExisted
pageExisted()
Determines whether the page being edited already existed.
Definition: DerivedPageDataUpdater.php:486
ParserOutput
Definition: ParserOutput.php:25
MediaWiki\Storage\DerivedPageDataUpdater\getParentRevision
getParentRevision()
Returns the parent revision of the new revision wrapped by this update.
Definition: DerivedPageDataUpdater.php:501
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:920
MediaWiki\Storage\DerivedPageDataUpdater\$articleCountMethod
string $articleCountMethod
see $wgArticleCountMethod
Definition: DerivedPageDataUpdater.php:159
MediaWiki\Storage\DerivedPageDataUpdater\prepareContent
prepareContent(User $user, RevisionSlotsUpdate $slotsUpdate, $useStash=true)
Prepare updates based on an update which has not yet been saved.
Definition: DerivedPageDataUpdater.php:754
MediaWiki\Storage\DerivedPageDataUpdater\getSlotParserOutput
getSlotParserOutput( $role, $generateHtml=true)
Definition: DerivedPageDataUpdater.php:1326
MediaWiki\Storage\DerivedPageDataUpdater\$user
UserIdentity null $user
Definition: DerivedPageDataUpdater.php:109
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:157
RecentChangesUpdateJob
Job for pruning recent changes.
Definition: RecentChangesUpdateJob.php:29
MediaWiki\Storage\DerivedPageDataUpdater\prepareUpdate
prepareUpdate(RevisionRecord $revision, array $options=[])
Prepare derived data updates targeting the given Revision.
Definition: DerivedPageDataUpdater.php:1118
MediaWiki\Storage\DerivedPageDataUpdater\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: DerivedPageDataUpdater.php:253
MediaWiki\Storage\DerivedPageDataUpdater\setArticleCountMethod
setArticleCountMethod( $articleCountMethod)
Definition: DerivedPageDataUpdater.php:451
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(Title $title, ?RevisionRecord $old, ?RevisionRecord $new, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Definition: ResourceLoaderWikiModule.php:542
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:81
MediaWiki\Storage\DerivedPageDataUpdater\getSlots
getSlots()
Returns the slots of the target revision, after PST.
Definition: DerivedPageDataUpdater.php:1012
DeferredUpdates\attemptUpdate
static attemptUpdate(DeferrableUpdate $update, ILBFactory $lbFactory)
Attempt to run an update with the appropriate transaction round state it expects.
Definition: DeferredUpdates.php:440
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:459
MediaWiki\Storage\DerivedPageDataUpdater\getPreparedEdit
getPreparedEdit()
Definition: DerivedPageDataUpdater.php:1300
Revision\MutableRevisionRecord\newFromParentRevision
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Definition: MutableRevisionRecord.php:56
ResourceLoaderWikiModule
Abstraction for ResourceLoader modules which pull from wiki pages.
Definition: ResourceLoaderWikiModule.php:55
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:442
SearchUpdate
Database independant search index updater.
Definition: SearchUpdate.php:33
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred update queue for execution at the appropriate time.
Definition: DeferredUpdates.php:106
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:53
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:37
MediaWiki\Storage\DerivedPageDataUpdater\getPageId
getPageId()
Definition: DerivedPageDataUpdater.php:593
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:538
Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:191
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:594
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
Title\equals
equals(LinkTarget $title)
Compare with another title.
Definition: Title.php:3876
MediaWiki\Storage\DerivedPageDataUpdater\isRedirect
isRedirect()
Definition: DerivedPageDataUpdater.php:710
MediaWiki\Storage\DerivedPageDataUpdater\getCanonicalParserOptions
getCanonicalParserOptions()
Definition: DerivedPageDataUpdater.php:1343
MediaWiki\Storage\DerivedPageDataUpdater\doUpdates
doUpdates()
Do standard updates after page edit, purge, or import.
Definition: DerivedPageDataUpdater.php:1457
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
MediaWiki\Storage\DerivedPageDataUpdater\$revisionStore
RevisionStore $revisionStore
Definition: DerivedPageDataUpdater.php:124
MediaWiki\Storage\DerivedPageDataUpdater\assertHasRevision
assertHasRevision( $method)
Definition: DerivedPageDataUpdater.php:951
Revision
Definition: Revision.php:40
MediaWiki\MediaWikiServices\getInstance
static getInstance()
Returns the global default instance of the top level service locator.
Definition: MediaWikiServices.php:188
MediaWiki\Storage\DerivedPageDataUpdater\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: DerivedPageDataUpdater.php:241
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:388
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1027
MediaWiki\Storage\DerivedPageDataUpdater\setLogger
setLogger(LoggerInterface $logger)
Definition: DerivedPageDataUpdater.php:338
ParserOptions\newFromUserAndLang
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
Definition: ParserOptions.php:1090
Revision\RevisionRecord\getUser
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Definition: RevisionRecord.php:371
MediaWiki\Storage\DerivedPageDataUpdater\getRevisionSlotsUpdate
getRevisionSlotsUpdate()
Returns the RevisionSlotsUpdate for this updater.
Definition: DerivedPageDataUpdater.php:1022
DeferredUpdates
Class for managing the deferred updates.
Definition: DeferredUpdates.php:62
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:262
MediaWiki\Storage\DerivedPageDataUpdater\getContentHandler
getContentHandler( $role)
Definition: DerivedPageDataUpdater.php:652
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:409
MediaWiki\Storage\DerivedPageDataUpdater\assertPrepared
assertPrepared( $method)
Definition: DerivedPageDataUpdater.php:943
User\isIP
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:948
MediaWiki\Storage\DerivedPageDataUpdater\getContentModel
getContentModel( $role)
Returns the content model of the given slot.
Definition: DerivedPageDataUpdater.php:643
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:85
Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:60
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:988
SiteStatsUpdate\factory
static factory(array $deltas)
Definition: SiteStatsUpdate.php:71
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:964
MediaWiki\Storage\DerivedPageDataUpdater\assertTransition
assertTransition( $newStage)
Asserts that a transition to the given stage is possible, without performing it.
Definition: DerivedPageDataUpdater.php:371
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:910
Revision\RevisionRecord\getPageId
getPageId()
Get the page ID.
Definition: RevisionRecord.php:331
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:899
MediaWiki\Storage\DerivedPageDataUpdater\useMaster
useMaster()
Definition: DerivedPageDataUpdater.php:657
MediaWiki\Storage\EditResult
Object for storing information about the effects of an edit.
Definition: EditResult.php:38
Revision\SlotRecord\newUnsaved
static newUnsaved( $role, Content $content)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:129
CategoryMembershipChangeJob
Job to add recent change entries mentioning category membership changes.
Definition: CategoryMembershipChangeJob.php:41
Revision\RevisionRecord\getId
getId()
Get revision ID.
Definition: RevisionRecord.php:279
Revision\RevisionRecord\getParentId
getParentId()
Get parent revision ID (the original previous page revision).
Definition: RevisionRecord.php:295
MediaWiki\Storage\RevisionSlotsUpdate
Value object representing a modification of revision slots.
Definition: RevisionSlotsUpdate.php:36
Revision\RevisionRenderer
The RevisionRenderer service provides access to rendered output for revisions.
Definition: RevisionRenderer.php:45
$content
$content
Definition: router.php:76
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:72
MediaWiki\Storage\DerivedPageDataUpdater\isContentDeleted
isContentDeleted()
Whether the content is deleted and thus not visible to the public.
Definition: DerivedPageDataUpdater.php:603
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:45
MediaWiki\Storage\DerivedPageDataUpdater\getWikiPage
getWikiPage()
Definition: DerivedPageDataUpdater.php:474
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)
Definition: DerivedPageDataUpdater.php:305
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:665
MediaWiki\Storage\DerivedPageDataUpdater\isContentPrepared
isContentPrepared()
Whether prepareUpdate() or prepareContent() have been called on this instance.
Definition: DerivedPageDataUpdater.php:575
LinksDeletionUpdate
Update object handling the cleanup of links tables after a page was deleted.
Definition: LinksDeletionUpdate.php:28
MediaWiki\Storage\DerivedPageDataUpdater\$loadbalancerFactory
ILBFactory $loadbalancerFactory
Definition: DerivedPageDataUpdater.php:144
Revision\RevisionRecord\getComment
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it's available to the specified audience.
Definition: RevisionRecord.php:396
MediaWiki\Storage\DerivedPageDataUpdater\getRemovedSlotRoles
getRemovedSlotRoles()
Returns the role names of the slots removed by the new revision.
Definition: DerivedPageDataUpdater.php:1060
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
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:41
MediaWiki\Storage\DerivedPageDataUpdater\$messageCache
MessageCache $messageCache
Definition: DerivedPageDataUpdater.php:139
MediaWiki\Storage\DerivedPageDataUpdater\doTransition
doTransition( $newStage)
Transition function for managing the life cycle of this instances.
Definition: DerivedPageDataUpdater.php:353
Revision\RevisionRecord\getSlots
getSlots()
Returns the slots defined for this revision.
Definition: RevisionRecord.php:233
MediaWiki\Storage\DerivedPageDataUpdater\getModifiedSlotRoles
getModifiedSlotRoles()
Returns the role names of the slots modified by the new revision, not including removed roles.
Definition: DerivedPageDataUpdater.php:1051
Content
Base interface for content objects.
Definition: Content.php:35
MediaWiki\Storage\DerivedPageDataUpdater\$jobQueueGroup
JobQueueGroup $jobQueueGroup
Definition: DerivedPageDataUpdater.php:134
Revision\RevisionRecord\getVisibility
getVisibility()
Get the deletion bitfield of the revision.
Definition: RevisionRecord.php:431
Title
Represents a title within MediaWiki.
Definition: Title.php:41
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\getRawContent
getRawContent( $role)
Returns the content of the given slot, with no audience checks.
Definition: DerivedPageDataUpdater.php:633
DeferredUpdates\PRESEND
const PRESEND
Definition: DeferredUpdates.php:84
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:466
MediaWiki\Storage\DerivedPageDataUpdater\getSecondaryDataUpdates
getSecondaryDataUpdates( $recursive=false)
Definition: DerivedPageDataUpdater.php:1352
MediaWiki\Storage\DerivedPageDataUpdater\doParserCacheUpdate
doParserCacheUpdate()
Definition: DerivedPageDataUpdater.php:1712
Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:49
MediaWiki\Storage\DerivedPageDataUpdater\getRenderedRevision
getRenderedRevision()
Definition: DerivedPageDataUpdater.php:928
MediaWiki\Storage\DerivedPageDataUpdater\$editResultCache
EditResultCache $editResultCache
Definition: DerivedPageDataUpdater.php:289
Revision\RevisionSlots
Value object representing the set of slots belonging to a revision.
Definition: RevisionSlots.php:41
MediaWiki\Storage\DerivedPageDataUpdater\$rcWatchCategoryMembership
boolean $rcWatchCategoryMembership
see $wgRCWatchCategoryMembership
Definition: DerivedPageDataUpdater.php:164
ParserCache
Definition: ParserCache.php:32
RecentChangesUpdateJob\newPurgeJob
static newPurgeJob()
Definition: RecentChangesUpdateJob.php:44
Revision\RevisionRecord\getContent
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
Definition: RevisionRecord.php:167
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:621
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:44
Revision\RevisionRecord\hasSameContent
hasSameContent(RevisionRecord $rec)
Definition: RevisionRecord.php:127
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:1676
Revision\RenderedRevision
RenderedRevision represents the rendered representation of a revision.
Definition: RenderedRevision.php:43
CategoryMembershipChangeJob\newSpec
static newSpec(Title $title, $revisionTimestamp)
Definition: CategoryMembershipChangeJob.php:52
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:978
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:77
MediaWiki\Storage\DerivedPageDataUpdater\assertHasPageState
assertHasPageState( $method)
Definition: DerivedPageDataUpdater.php:934
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:570
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:37
MediaWiki\Storage\DerivedPageDataUpdater\$hookRunner
HookRunner $hookRunner
Definition: DerivedPageDataUpdater.php:149
MediaWiki\Storage\DerivedPageDataUpdater\getTouchedSlotRoles
getTouchedSlotRoles()
Returns the role names of the slots touched by the new revision, including removed roles.
Definition: DerivedPageDataUpdater.php:1041
WikiPage\onArticleEdit
static onArticleEdit(Title $title, $revRecord=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3557
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:1637
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:576
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:104
MediaWiki\Storage\DerivedPageDataUpdater\$wikiPage
WikiPage $wikiPage
Definition: DerivedPageDataUpdater.php:114
MessageCache
Cache of messages that are defined by MediaWiki namespace pages or by hooks.
Definition: MessageCache.php:50
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:55
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:145
MediaWiki\Storage\DerivedPageDataUpdater\revisionIsRedirect
revisionIsRedirect(RevisionRecord $rev)
Definition: DerivedPageDataUpdater.php:723
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:41
MediaWiki\Storage\DerivedPageDataUpdater\grabCurrentRevision
grabCurrentRevision()
Returns the revision that was the page's current revision when grabCurrentRevision() was first called...
Definition: DerivedPageDataUpdater.php:543
MediaWiki\Storage\DerivedPageDataUpdater\isUpdatePrepared
isUpdatePrepared()
Whether prepareUpdate() has been called on this instance.
Definition: DerivedPageDataUpdater.php:586
RevertedTagUpdateJob
Job for deferring the execution of RevertedTagUpdate.
Definition: RevertedTagUpdateJob.php:38
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
ParserOptions\newFromUser
static newFromUser( $user)
Get a ParserOptions object from a given user.
Definition: ParserOptions.php:1077
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:30