MediaWiki  master
DerivedPageDataUpdater.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Storage;
24 
26 use Content;
27 use ContentHandler;
28 use DataUpdate;
30 use DeferredUpdates;
31 use Hooks;
32 use IDBAccessObject;
33 use InvalidArgumentException;
34 use JobQueueGroup;
35 use Language;
37 use LinksUpdate;
38 use LogicException;
50 use MessageCache;
52 use ParserCache;
53 use ParserOptions;
54 use ParserOutput;
55 use Psr\Log\LoggerAwareInterface;
56 use Psr\Log\LoggerInterface;
57 use Psr\Log\NullLogger;
60 use Revision;
61 use SearchUpdate;
62 use SiteStatsUpdate;
63 use Title;
64 use User;
65 use Wikimedia\Assert\Assert;
67 use WikiPage;
68 
100 class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
101 
105  private $user = null;
106 
110  private $wikiPage;
111 
115  private $parserCache;
116 
120  private $revisionStore;
121 
125  private $contLang;
126 
130  private $jobQueueGroup;
131 
135  private $messageCache;
136 
141 
145  private $logger;
146 
151 
155  private $rcWatchCategoryMembership = false;
156 
161  private $options = [
162  'changed' => true,
163  // newrev is true if prepareUpdate is handling the creation of a new revision,
164  // as opposed to a null edit or a forced update.
165  'newrev' => false,
166  'created' => false,
167  'moved' => false,
168  'restored' => false,
169  'oldrevision' => null,
170  'oldcountable' => null,
171  'oldredirect' => null,
172  'triggeringUser' => null,
173  // causeAction/causeAgent default to 'unknown' but that's handled where it's read,
174  // to make the life of prepareUpdate() callers easier.
175  'causeAction' => null,
176  'causeAgent' => null,
177  ];
178 
198  private $pageState = null;
199 
203  private $slotsUpdate = null;
204 
208  private $parentRevision = null;
209 
213  private $revision = null;
214 
218  private $renderedRevision = null;
219 
224 
227 
236  private $stage = 'new';
237 
248  private const TRANSITIONS = [
249  'new' => [
250  'new' => true,
251  'knows-current' => true,
252  'has-content' => true,
253  'has-revision' => true,
254  ],
255  'knows-current' => [
256  'knows-current' => true,
257  'has-content' => true,
258  'has-revision' => true,
259  ],
260  'has-content' => [
261  'has-content' => true,
262  'has-revision' => true,
263  ],
264  'has-revision' => [
265  'has-revision' => true,
266  'done' => true,
267  ],
268  ];
269 
281  public function __construct(
291  ) {
292  $this->wikiPage = $wikiPage;
293 
294  $this->parserCache = $parserCache;
295  $this->revisionStore = $revisionStore;
296  $this->revisionRenderer = $revisionRenderer;
297  $this->slotRoleRegistry = $slotRoleRegistry;
298  $this->jobQueueGroup = $jobQueueGroup;
299  $this->messageCache = $messageCache;
300  $this->contLang = $contLang;
301  // XXX only needed for waiting for replicas to catch up; there should be a narrower
302  // interface for that.
303  $this->loadbalancerFactory = $loadbalancerFactory;
304  $this->logger = new NullLogger();
305  }
306 
307  public function setLogger( LoggerInterface $logger ) {
308  $this->logger = $logger;
309  }
310 
322  private function doTransition( $newStage ) {
323  $this->assertTransition( $newStage );
324 
325  $oldStage = $this->stage;
326  $this->stage = $newStage;
327 
328  return $oldStage;
329  }
330 
340  private function assertTransition( $newStage ) {
341  if ( empty( self::TRANSITIONS[$this->stage][$newStage] ) ) {
342  throw new LogicException( "Cannot transition from {$this->stage} to $newStage" );
343  }
344  }
345 
357  public function isReusableFor(
358  UserIdentity $user = null,
359  RevisionRecord $revision = null,
361  $parentId = null
362  ) {
363  if ( $revision
364  && $parentId
365  && $revision->getParentId() !== $parentId
366  ) {
367  throw new InvalidArgumentException( '$parentId should match the parent of $revision' );
368  }
369 
370  // NOTE: For null revisions, $user may be different from $this->revision->getUser
371  // and also from $revision->getUser.
372  // But $user should always match $this->user.
373  if ( $user && $this->user && $user->getName() !== $this->user->getName() ) {
374  return false;
375  }
376 
377  if ( $revision && $this->revision && $this->revision->getId()
378  && $this->revision->getId() !== $revision->getId()
379  ) {
380  return false;
381  }
382 
383  if ( $this->pageState
384  && $revision
385  && $revision->getParentId() !== null
386  && $this->pageState['oldId'] !== $revision->getParentId()
387  ) {
388  return false;
389  }
390 
391  if ( $this->pageState
392  && $parentId !== null
393  && $this->pageState['oldId'] !== $parentId
394  ) {
395  return false;
396  }
397 
398  // NOTE: this check is the primary reason for having the $this->slotsUpdate field!
399  if ( $this->slotsUpdate
400  && $slotsUpdate
401  && !$this->slotsUpdate->hasSameUpdates( $slotsUpdate )
402  ) {
403  return false;
404  }
405 
406  if ( $revision
407  && $this->revision
408  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
409  ) {
410  return false;
411  }
412 
413  return true;
414  }
415 
421  $this->articleCountMethod = $articleCountMethod;
422  }
423 
429  $this->rcWatchCategoryMembership = $rcWatchCategoryMembership;
430  }
431 
435  private function getTitle() {
436  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
437  return $this->wikiPage->getTitle();
438  }
439 
443  private function getWikiPage() {
444  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
445  return $this->wikiPage;
446  }
447 
455  public function pageExisted() {
456  $this->assertHasPageState( __METHOD__ );
457 
458  return $this->pageState['oldId'] > 0;
459  }
460 
470  private function getParentRevision() {
471  $this->assertPrepared( __METHOD__ );
472 
473  if ( $this->parentRevision ) {
474  return $this->parentRevision;
475  }
476 
477  if ( !$this->pageState['oldId'] ) {
478  // If there was no current revision, there is no parent revision,
479  // since the page didn't exist.
480  return null;
481  }
482 
483  $oldId = $this->revision->getParentId();
484  $flags = $this->useMaster() ? RevisionStore::READ_LATEST : 0;
485  $this->parentRevision = $oldId
486  ? $this->revisionStore->getRevisionById( $oldId, $flags )
487  : null;
488 
489  return $this->parentRevision;
490  }
491 
512  public function grabCurrentRevision() {
513  if ( $this->pageState ) {
514  return $this->pageState['oldRevision'];
515  }
516 
517  $this->assertTransition( 'knows-current' );
518 
519  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
520  $wikiPage = $this->getWikiPage();
521 
522  // Do not call WikiPage::clear(), since the caller may already have caused page data
523  // to be loaded with SELECT FOR UPDATE. Just assert it's loaded now.
524  $wikiPage->loadPageData( self::READ_LATEST );
525  $rev = $wikiPage->getRevision();
526  $current = $rev ? $rev->getRevisionRecord() : null;
527 
528  $this->pageState = [
529  'oldRevision' => $current,
530  'oldId' => $rev ? $rev->getId() : 0,
531  'oldIsRedirect' => $wikiPage->isRedirect(), // NOTE: uses page table
532  'oldCountable' => $wikiPage->isCountable(), // NOTE: uses pagelinks table
533  ];
534 
535  $this->doTransition( 'knows-current' );
536 
537  return $this->pageState['oldRevision'];
538  }
539 
545  public function isContentPrepared() {
546  return $this->revision !== null;
547  }
548 
556  public function isUpdatePrepared() {
557  return $this->revision !== null && $this->revision->getId() !== null;
558  }
559 
563  private function getPageId() {
564  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
565  return $this->wikiPage->getId();
566  }
567 
573  public function isContentDeleted() {
574  if ( $this->revision ) {
575  return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
576  } else {
577  // If the content has not been saved yet, it cannot have been deleted yet.
578  return false;
579  }
580  }
581 
591  public function getRawSlot( $role ) {
592  return $this->getSlots()->getSlot( $role );
593  }
594 
603  public function getRawContent( $role ) {
604  return $this->getRawSlot( $role )->getContent();
605  }
606 
613  private function getContentModel( $role ) {
614  return $this->getRawSlot( $role )->getModel();
615  }
616 
621  private function getContentHandler( $role ) {
622  // TODO: inject something like a ContentHandlerRegistry
623  return ContentHandler::getForModelID( $this->getContentModel( $role ) );
624  }
625 
626  private function useMaster() {
627  // TODO: can we just set a flag to true in prepareContent()?
628  return $this->wikiPage->wasLoadedFrom( self::READ_LATEST );
629  }
630 
634  public function isCountable() {
635  // NOTE: Keep in sync with WikiPage::isCountable.
636 
637  if ( !$this->getTitle()->isContentPage() ) {
638  return false;
639  }
640 
641  if ( $this->isContentDeleted() ) {
642  // This should be irrelevant: countability only applies to the current revision,
643  // and the current revision is never suppressed.
644  return false;
645  }
646 
647  if ( $this->isRedirect() ) {
648  return false;
649  }
650 
651  $hasLinks = null;
652 
653  if ( $this->articleCountMethod === 'link' ) {
654  // NOTE: it would be more appropriate to determine for each slot separately
655  // whether it has links, and use that information with that slot's
656  // isCountable() method. However, that would break parity with
657  // WikiPage::isCountable, which uses the pagelinks table to determine
658  // whether the current revision has links.
659  $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() );
660  }
661 
662  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
663  $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
664  if ( $roleHandler->supportsArticleCount() ) {
665  $content = $this->getRawContent( $role );
666 
667  if ( $content->isCountable( $hasLinks ) ) {
668  return true;
669  }
670  }
671  }
672 
673  return false;
674  }
675 
679  public function isRedirect() {
680  // NOTE: main slot determines redirect status
681  // TODO: MCR: this should be controlled by a PageTypeHandler
682  $mainContent = $this->getRawContent( SlotRecord::MAIN );
683 
684  return $mainContent->isRedirect();
685  }
686 
692  private function revisionIsRedirect( RevisionRecord $rev ) {
693  // NOTE: main slot determines redirect status
694  $mainContent = $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
695 
696  return $mainContent->isRedirect();
697  }
698 
723  public function prepareContent(
724  User $user,
726  $useStash = true
727  ) {
728  if ( $this->slotsUpdate ) {
729  if ( !$this->user ) {
730  throw new LogicException(
731  'Unexpected state: $this->slotsUpdate was initialized, '
732  . 'but $this->user was not.'
733  );
734  }
735 
736  if ( $this->user->getName() !== $user->getName() ) {
737  throw new LogicException( 'Can\'t call prepareContent() again for different user! '
738  . 'Expected ' . $this->user->getName() . ', got ' . $user->getName()
739  );
740  }
741 
742  if ( !$this->slotsUpdate->hasSameUpdates( $slotsUpdate ) ) {
743  throw new LogicException(
744  'Can\'t call prepareContent() again with different slot content!'
745  );
746  }
747 
748  return; // prepareContent() already done, nothing to do
749  }
750 
751  $this->assertTransition( 'has-content' );
752 
753  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
754  $title = $this->getTitle();
755 
757 
758  // The edit may have already been prepared via api.php?action=stashedit
759  $stashedEdit = false;
760 
761  // TODO: MCR: allow output for all slots to be stashed.
762  if ( $useStash && $slotsUpdate->isModifiedSlot( SlotRecord::MAIN ) ) {
763  $editStash = MediaWikiServices::getInstance()->getPageEditStash();
764  $stashedEdit = $editStash->checkCache(
765  $title,
768  );
769  }
770 
771  $userPopts = ParserOptions::newFromUserAndLang( $user, $this->contLang );
772  Hooks::run( 'ArticlePrepareTextForEdit', [ $wikiPage, $userPopts ] );
773 
774  $this->user = $user;
775  $this->slotsUpdate = $slotsUpdate;
776 
777  if ( $parentRevision ) {
779  } else {
780  $this->revision = new MutableRevisionRecord( $title );
781  }
782 
783  // NOTE: user and timestamp must be set, so they can be used for
784  // {{subst:REVISIONUSER}} and {{subst:REVISIONTIMESTAMP}} in PST!
785  $this->revision->setTimestamp( wfTimestampNow() );
786  $this->revision->setUser( $user );
787 
788  // Set up ParserOptions to operate on the new revision
789  $oldCallback = $userPopts->getCurrentRevisionCallback();
790  $userPopts->setCurrentRevisionCallback(
791  function ( Title $parserTitle, $parser = false ) use ( $title, $oldCallback ) {
792  if ( $parserTitle->equals( $title ) ) {
793  $legacyRevision = new Revision( $this->revision );
794  return $legacyRevision;
795  } else {
796  return call_user_func( $oldCallback, $parserTitle, $parser );
797  }
798  }
799  );
800 
801  $pstContentSlots = $this->revision->getSlots();
802 
803  foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
804  $slot = $slotsUpdate->getModifiedSlot( $role );
805 
806  if ( $slot->isInherited() ) {
807  // No PST for inherited slots! Note that "modified" slots may still be inherited
808  // from an earlier version, e.g. for rollbacks.
809  $pstSlot = $slot;
810  } elseif ( $role === SlotRecord::MAIN && $stashedEdit ) {
811  // TODO: MCR: allow PST content for all slots to be stashed.
812  $pstSlot = SlotRecord::newUnsaved( $role, $stashedEdit->pstContent );
813  } else {
814  $content = $slot->getContent();
815  $pstContent = $content->preSaveTransform( $title, $this->user, $userPopts );
816  $pstSlot = SlotRecord::newUnsaved( $role, $pstContent );
817  }
818 
819  $pstContentSlots->setSlot( $pstSlot );
820  }
821 
822  foreach ( $slotsUpdate->getRemovedRoles() as $role ) {
823  $pstContentSlots->removeSlot( $role );
824  }
825 
826  $this->options['created'] = ( $parentRevision === null );
827  $this->options['changed'] = ( $parentRevision === null
828  || !$pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
829 
830  $this->doTransition( 'has-content' );
831 
832  if ( !$this->options['changed'] ) {
833  // null-edit!
834 
835  // TODO: move this into MutableRevisionRecord
836  // TODO: This needs to behave differently for a forced dummy edit!
837  $this->revision->setId( $parentRevision->getId() );
838  $this->revision->setTimestamp( $parentRevision->getTimestamp() );
839  $this->revision->setPageId( $parentRevision->getPageId() );
840  $this->revision->setParentId( $parentRevision->getParentId() );
841  $this->revision->setUser( $parentRevision->getUser( RevisionRecord::RAW ) );
842  $this->revision->setComment( $parentRevision->getComment( RevisionRecord::RAW ) );
843  $this->revision->setMinorEdit( $parentRevision->isMinor() );
844  $this->revision->setVisibility( $parentRevision->getVisibility() );
845 
846  // prepareUpdate() is redundant for null-edits
847  $this->doTransition( 'has-revision' );
848  } else {
849  $this->parentRevision = $parentRevision;
850  }
851 
852  $renderHints = [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ];
853 
854  if ( $stashedEdit ) {
856  $output = $stashedEdit->output;
857  // TODO: this should happen when stashing the ParserOutput, not now!
858  $output->setCacheTime( $stashedEdit->timestamp );
859 
860  $renderHints['known-revision-output'] = $output;
861 
862  $this->logger->debug( __METHOD__ . ': using stashed edit output...' );
863  }
864 
865  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
866  // NOTE: the revision is either new or current, so we can bypass audience checks.
867  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
868  $this->revision,
869  null,
870  null,
871  $renderHints
872  );
873  }
874 
890  public function getRevision() {
891  $this->assertPrepared( __METHOD__ );
892  return $this->revision;
893  }
894 
898  public function getRenderedRevision() {
899  $this->assertPrepared( __METHOD__ );
900 
902  }
903 
904  private function assertHasPageState( $method ) {
905  if ( !$this->pageState ) {
906  throw new LogicException(
907  'Must call grabCurrentRevision() or prepareContent() '
908  . 'or prepareUpdate() before calling ' . $method
909  );
910  }
911  }
912 
913  private function assertPrepared( $method ) {
914  if ( !$this->revision ) {
915  throw new LogicException(
916  'Must call prepareContent() or prepareUpdate() before calling ' . $method
917  );
918  }
919  }
920 
921  private function assertHasRevision( $method ) {
922  if ( !$this->revision->getId() ) {
923  throw new LogicException(
924  'Must call prepareUpdate() before calling ' . $method
925  );
926  }
927  }
928 
934  public function isCreation() {
935  $this->assertPrepared( __METHOD__ );
936  return $this->options['created'];
937  }
938 
948  public function isChange() {
949  $this->assertPrepared( __METHOD__ );
950  return $this->options['changed'];
951  }
952 
958  public function wasRedirect() {
959  $this->assertHasPageState( __METHOD__ );
960 
961  if ( $this->pageState['oldIsRedirect'] === null ) {
963  $rev = $this->pageState['oldRevision'];
964  if ( $rev ) {
965  $this->pageState['oldIsRedirect'] = $this->revisionIsRedirect( $rev );
966  } else {
967  $this->pageState['oldIsRedirect'] = false;
968  }
969  }
970 
971  return $this->pageState['oldIsRedirect'];
972  }
973 
982  public function getSlots() {
983  $this->assertPrepared( __METHOD__ );
984  return $this->revision->getSlots();
985  }
986 
992  private function getRevisionSlotsUpdate() {
993  $this->assertPrepared( __METHOD__ );
994 
995  if ( !$this->slotsUpdate ) {
996  $old = $this->getParentRevision();
997  $this->slotsUpdate = RevisionSlotsUpdate::newFromRevisionSlots(
998  $this->revision->getSlots(),
999  $old ? $old->getSlots() : null
1000  );
1001  }
1002  return $this->slotsUpdate;
1003  }
1004 
1011  public function getTouchedSlotRoles() {
1012  return $this->getRevisionSlotsUpdate()->getTouchedRoles();
1013  }
1014 
1021  public function getModifiedSlotRoles() {
1022  return $this->getRevisionSlotsUpdate()->getModifiedRoles();
1023  }
1024 
1030  public function getRemovedSlotRoles() {
1031  return $this->getRevisionSlotsUpdate()->getRemovedRoles();
1032  }
1033 
1082  public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
1083  Assert::parameter(
1084  !isset( $options['oldrevision'] )
1085  || $options['oldrevision'] instanceof Revision
1086  || $options['oldrevision'] instanceof RevisionRecord,
1087  '$options["oldrevision"]',
1088  'must be a RevisionRecord (or Revision)'
1089  );
1090  Assert::parameter(
1091  !isset( $options['triggeringUser'] )
1092  || $options['triggeringUser'] instanceof UserIdentity,
1093  '$options["triggeringUser"]',
1094  'must be a UserIdentity'
1095  );
1096 
1097  if ( !$revision->getId() ) {
1098  throw new InvalidArgumentException(
1099  'Revision must have an ID set for it to be used with prepareUpdate()!'
1100  );
1101  }
1102 
1103  if ( $this->revision && $this->revision->getId() ) {
1104  if ( $this->revision->getId() === $revision->getId() ) {
1105  return; // nothing to do!
1106  } else {
1107  throw new LogicException(
1108  'Trying to re-use DerivedPageDataUpdater with revision '
1109  . $revision->getId()
1110  . ', but it\'s already bound to revision '
1111  . $this->revision->getId()
1112  );
1113  }
1114  }
1115 
1116  if ( $this->revision
1117  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
1118  ) {
1119  throw new LogicException(
1120  'The Revision provided has mismatching content!'
1121  );
1122  }
1123 
1124  // Override fields defined in $this->options with values from $options.
1125  $this->options = array_intersect_key( $options, $this->options ) + $this->options;
1126 
1127  if ( $this->revision ) {
1128  $oldId = $this->pageState['oldId'] ?? 0;
1129  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1130  } elseif ( isset( $this->options['oldrevision'] ) ) {
1132  $oldRev = $this->options['oldrevision'];
1133  $oldId = $oldRev->getId();
1134  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1135  } else {
1136  $oldId = $revision->getParentId();
1137  }
1138 
1139  if ( $oldId !== null ) {
1140  // XXX: what if $options['changed'] disagrees?
1141  // MovePage creates a dummy revision with changed = false!
1142  // We may want to explicitly distinguish between "no new revision" (null-edit)
1143  // and "new revision without new content" (dummy revision).
1144 
1145  if ( $oldId === $revision->getParentId() ) {
1146  // NOTE: this may still be a NullRevision!
1147  // New revision!
1148  $this->options['changed'] = true;
1149  } elseif ( $oldId === $revision->getId() ) {
1150  // Null-edit!
1151  $this->options['changed'] = false;
1152  } else {
1153  // This indicates that calling code has given us the wrong Revision object
1154  throw new LogicException(
1155  'The Revision mismatches old revision ID: '
1156  . 'Old ID is ' . $oldId
1157  . ', parent ID is ' . $revision->getParentId()
1158  . ', revision ID is ' . $revision->getId()
1159  );
1160  }
1161  }
1162 
1163  // If prepareContent() was used to generate the PST content (which is indicated by
1164  // $this->slotsUpdate being set), and this is not a null-edit, then the given
1165  // revision must have the acting user as the revision author. Otherwise, user
1166  // signatures generated by PST would mismatch the user in the revision record.
1167  if ( $this->user !== null && $this->options['changed'] && $this->slotsUpdate ) {
1168  $user = $revision->getUser();
1169  if ( !$this->user->equals( $user ) ) {
1170  throw new LogicException(
1171  'The Revision provided has a mismatching actor: expected '
1172  . $this->user->getName()
1173  . ', got '
1174  . $user->getName()
1175  );
1176  }
1177  }
1178 
1179  // If $this->pageState was not yet initialized by grabCurrentRevision or prepareContent,
1180  // emulate the state of the page table before the edit, as good as we can.
1181  if ( !$this->pageState ) {
1182  $this->pageState = [
1183  'oldIsRedirect' => isset( $this->options['oldredirect'] )
1184  && is_bool( $this->options['oldredirect'] )
1185  ? $this->options['oldredirect']
1186  : null,
1187  'oldCountable' => isset( $this->options['oldcountable'] )
1188  && is_bool( $this->options['oldcountable'] )
1189  ? $this->options['oldcountable']
1190  : null,
1191  ];
1192 
1193  if ( $this->options['changed'] ) {
1194  // The edit created a new revision
1195  $this->pageState['oldId'] = $revision->getParentId();
1196 
1197  if ( isset( $this->options['oldrevision'] ) ) {
1198  $rev = $this->options['oldrevision'];
1199  $this->pageState['oldRevision'] = $rev instanceof Revision
1200  ? $rev->getRevisionRecord()
1201  : $rev;
1202  }
1203  } else {
1204  // This is a null-edit, so the old revision IS the new revision!
1205  $this->pageState['oldId'] = $revision->getId();
1206  $this->pageState['oldRevision'] = $revision;
1207  }
1208  }
1209 
1210  // "created" is forced here
1211  $this->options['created'] = ( $this->options['created'] ||
1212  ( $this->pageState['oldId'] === 0 ) );
1213 
1214  $this->revision = $revision;
1215 
1216  $this->doTransition( 'has-revision' );
1217 
1218  // NOTE: in case we have a User object, don't override with a UserIdentity.
1219  // We already checked that $revision->getUser() mathces $this->user;
1220  if ( !$this->user ) {
1221  $this->user = $revision->getUser( RevisionRecord::RAW );
1222  }
1223 
1224  // Prune any output that depends on the revision ID.
1225  if ( $this->renderedRevision ) {
1226  $this->renderedRevision->updateRevision( $revision );
1227  } else {
1228  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
1229  // NOTE: the revision is either new or current, so we can bypass audience checks.
1230  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
1231  $this->revision,
1232  null,
1233  null,
1234  [
1235  'use-master' => $this->useMaster(),
1236  'audience' => RevisionRecord::RAW,
1237  'known-revision-output' => $options['known-revision-output'] ?? null
1238  ]
1239  );
1240 
1241  // XXX: Since we presumably are dealing with the current revision,
1242  // we could try to get the ParserOutput from the parser cache.
1243  }
1244 
1245  // TODO: optionally get ParserOutput from the ParserCache here.
1246  // Move the logic used by RefreshLinksJob here!
1247  }
1248 
1253  public function getPreparedEdit() {
1254  $this->assertPrepared( __METHOD__ );
1255 
1257  $preparedEdit = new PreparedEdit();
1258 
1259  $preparedEdit->popts = $this->getCanonicalParserOptions();
1260  $preparedEdit->parserOutputCallback = [ $this, 'getCanonicalParserOutput' ];
1261  $preparedEdit->pstContent = $this->revision->getContent( SlotRecord::MAIN );
1262  $preparedEdit->newContent =
1264  ? $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent()
1265  : $this->revision->getContent( SlotRecord::MAIN ); // XXX: can we just remove this?
1266  $preparedEdit->oldContent = null; // unused. // XXX: could get this from the parent revision
1267  $preparedEdit->revid = $this->revision ? $this->revision->getId() : null;
1268  $preparedEdit->timestamp = $preparedEdit->output->getCacheTime();
1269  $preparedEdit->format = $preparedEdit->pstContent->getDefaultFormat();
1270 
1271  return $preparedEdit;
1272  }
1273 
1279  public function getSlotParserOutput( $role, $generateHtml = true ) {
1280  return $this->getRenderedRevision()->getSlotParserOutput(
1281  $role,
1282  [ 'generate-html' => $generateHtml ]
1283  );
1284  }
1285 
1289  public function getCanonicalParserOutput() {
1290  return $this->getRenderedRevision()->getRevisionParserOutput();
1291  }
1292 
1296  public function getCanonicalParserOptions() {
1297  return $this->getRenderedRevision()->getOptions();
1298  }
1299 
1305  public function getSecondaryDataUpdates( $recursive = false ) {
1306  if ( $this->isContentDeleted() ) {
1307  // This shouldn't happen, since the current content is always public,
1308  // and DataUpates are only needed for current content.
1309  return [];
1310  }
1311 
1312  $output = $this->getCanonicalParserOutput();
1313 
1314  // Construct a LinksUpdate for the combined canonical output.
1315  $linksUpdate = new LinksUpdate(
1316  $this->getTitle(),
1317  $output,
1318  $recursive
1319  );
1320 
1321  $allUpdates = [ $linksUpdate ];
1322 
1323  // NOTE: Run updates for all slots, not just the modified slots! Otherwise,
1324  // info for an inherited slot may end up being removed. This is also needed
1325  // to ensure that purges are effective.
1327  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
1328  $slot = $this->getRawSlot( $role );
1329  $content = $slot->getContent();
1330  $handler = $content->getContentHandler();
1331 
1332  $updates = $handler->getSecondaryDataUpdates(
1333  $this->getTitle(),
1334  $content,
1335  $role,
1337  );
1338  $allUpdates = array_merge( $allUpdates, $updates );
1339 
1340  // TODO: remove B/C hack in 1.32!
1341  // NOTE: we assume that the combined output contains all relevant meta-data for
1342  // all slots!
1343  $legacyUpdates = $content->getSecondaryDataUpdates(
1344  $this->getTitle(),
1345  null,
1346  $recursive,
1347  $output
1348  );
1349 
1350  // HACK: filter out redundant and incomplete LinksUpdates
1351  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1352  return !( $update instanceof LinksUpdate );
1353  } );
1354 
1355  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1356  }
1357 
1358  // XXX: if a slot was removed by an earlier edit, but deletion updates failed to run at
1359  // that time, we don't know for which slots to run deletion updates when purging a page.
1360  // We'd have to examine the entire history of the page to determine that. Perhaps there
1361  // could be a "try extra hard" mode for that case that would run a DB query to find all
1362  // roles/models ever used on the page. On the other hand, removing slots should be quite
1363  // rare, so perhaps this isn't worth the trouble.
1364 
1365  // TODO: consolidate with similar logic in WikiPage::getDeletionUpdates()
1366  $wikiPage = $this->getWikiPage();
1368  foreach ( $this->getRemovedSlotRoles() as $role ) {
1369  // HACK: we should get the content model of the removed slot from a SlotRoleHandler!
1370  // For now, find the slot in the parent revision - if the slot was removed, it should
1371  // always exist in the parent revision.
1372  $parentSlot = $parentRevision->getSlot( $role, RevisionRecord::RAW );
1373  $content = $parentSlot->getContent();
1374  $handler = $content->getContentHandler();
1375 
1376  $updates = $handler->getDeletionUpdates(
1377  $this->getTitle(),
1378  $role
1379  );
1380  $allUpdates = array_merge( $allUpdates, $updates );
1381 
1382  // TODO: remove B/C hack in 1.32!
1383  $legacyUpdates = $content->getDeletionUpdates( $wikiPage );
1384 
1385  // HACK: filter out redundant and incomplete LinksDeletionUpdate
1386  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1387  return !( $update instanceof LinksDeletionUpdate );
1388  } );
1389 
1390  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1391  }
1392 
1393  // TODO: hard deprecate SecondaryDataUpdates in favor of RevisionDataUpdates in 1.33!
1394  Hooks::run(
1395  'RevisionDataUpdates',
1396  [ $this->getTitle(), $renderedRevision, &$allUpdates ]
1397  );
1398 
1399  return $allUpdates;
1400  }
1401 
1412  public function doUpdates() {
1413  $this->assertTransition( 'done' );
1414 
1415  // TODO: move logic into a PageEventEmitter service
1416 
1417  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
1418 
1419  $legacyUser = User::newFromIdentity( $this->user );
1420  $legacyRevision = new Revision( $this->revision );
1421 
1422  $userParserOptions = ParserOptions::newFromUser( $legacyUser );
1423  // Decide whether to save the final canonical parser ouput based on the fact that
1424  // users are typically redirected to viewing pages right after they edit those pages.
1425  // Due to vary-revision-id, getting/saving that output here might require a reparse.
1426  if ( $userParserOptions->matchesForCacheKey( $this->getCanonicalParserOptions() ) ) {
1427  // Whether getting the final output requires a reparse or not, the user will
1428  // need canonical output anyway, since that is what their parser options use.
1429  // A reparse now at least has the benefit of various warm process caches.
1430  $this->doParserCacheUpdate();
1431  } else {
1432  // If the user does not have canonical parse options, then don't risk another parse
1433  // to make output they cannot use on the page refresh that typically occurs after
1434  // editing. Doing the parser output save post-send will still benefit *other* users.
1435  DeferredUpdates::addCallableUpdate( function () {
1436  $this->doParserCacheUpdate();
1437  } );
1438  }
1439 
1440  // Defer the getCannonicalParserOutput() call triggered by getSecondaryDataUpdates()
1441  // by wrapping the code that schedules the secondary updates in a callback itself
1442  $wrapperUpdate = new MWCallableUpdate(
1443  function () {
1444  $this->doSecondaryDataUpdates( [
1445  // T52785 do not update any other pages on a null edit
1446  'recursive' => $this->options['changed']
1447  ] );
1448  },
1449  __METHOD__
1450  );
1451  $wrapperUpdate->setTransactionRoundRequirement( $wrapperUpdate::TRX_ROUND_ABSENT );
1452  DeferredUpdates::addUpdate( $wrapperUpdate );
1453 
1454  // TODO: MCR: check if *any* changed slot supports categories!
1455  if ( $this->rcWatchCategoryMembership
1456  && $this->getContentHandler( SlotRecord::MAIN )->supportsCategories() === true
1457  && ( $this->options['changed'] || $this->options['created'] )
1458  && !$this->options['restored']
1459  ) {
1460  // Note: jobs are pushed after deferred updates, so the job should be able to see
1461  // the recent change entry (also done via deferred updates) and carry over any
1462  // bot/deletion/IP flags, ect.
1463  $this->jobQueueGroup->lazyPush(
1465  $this->getTitle(),
1466  $this->revision->getTimestamp()
1467  )
1468  );
1469  }
1470 
1471  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1472  // @note: Extensions should *avoid* calling getCannonicalParserOutput() when using
1473  // this hook whenever possible in order to avoid unnecessary additional parses.
1474  $editInfo = $this->getPreparedEdit();
1475  Hooks::run( 'ArticleEditUpdates',
1476  [ &$wikiPage, &$editInfo, $this->options['changed'] ] );
1477 
1478  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1479  if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
1480  // Flush old entries from the `recentchanges` table
1481  if ( mt_rand( 0, 9 ) == 0 ) {
1482  $this->jobQueueGroup->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
1483  }
1484  }
1485 
1486  $id = $this->getPageId();
1487  $title = $this->getTitle();
1488  $shortTitle = $title->getDBkey();
1489 
1490  if ( !$title->exists() ) {
1491  wfDebug( __METHOD__ . ": Page doesn't exist any more, bailing out\n" );
1492 
1493  $this->doTransition( 'done' );
1494  return;
1495  }
1496 
1497  DeferredUpdates::addCallableUpdate( function () {
1498  if (
1499  $this->options['oldcountable'] === 'no-change' ||
1500  ( !$this->options['changed'] && !$this->options['moved'] )
1501  ) {
1502  $good = 0;
1503  } elseif ( $this->options['created'] ) {
1504  $good = (int)$this->isCountable();
1505  } elseif ( $this->options['oldcountable'] !== null ) {
1506  $good = (int)$this->isCountable()
1507  - (int)$this->options['oldcountable'];
1508  } elseif ( isset( $this->pageState['oldCountable'] ) ) {
1509  $good = (int)$this->isCountable()
1510  - (int)$this->pageState['oldCountable'];
1511  } else {
1512  $good = 0;
1513  }
1514  $edits = $this->options['changed'] ? 1 : 0;
1515  $pages = $this->options['created'] ? 1 : 0;
1516 
1518  [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
1519  ) );
1520  } );
1521 
1522  // TODO: make search infrastructure aware of slots!
1523  $mainSlot = $this->revision->getSlot( SlotRecord::MAIN );
1524  if ( !$mainSlot->isInherited() && !$this->isContentDeleted() ) {
1525  DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $mainSlot->getContent() ) );
1526  }
1527 
1528  // If this is another user's talk page, update newtalk.
1529  // Don't do this if $options['changed'] = false (null-edits) nor if
1530  // it's a minor edit and the user making the edit doesn't generate notifications for those.
1531  if ( $this->options['changed']
1532  && $title->getNamespace() == NS_USER_TALK
1533  && $shortTitle != $legacyUser->getTitleKey()
1534  && !( $this->revision->isMinor() && MediaWikiServices::getInstance()
1535  ->getPermissionManager()
1536  ->userHasRight( $legacyUser, 'nominornewtalk' ) )
1537  ) {
1538  $recipient = User::newFromName( $shortTitle, false );
1539  if ( !$recipient ) {
1540  wfDebug( __METHOD__ . ": invalid username\n" );
1541  } else {
1542  // Allow extensions to prevent user notification
1543  // when a new message is added to their talk page
1544  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1545  if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
1546  if ( User::isIP( $shortTitle ) ) {
1547  // An anonymous user
1548  $recipient->setNewtalk( true, $legacyRevision );
1549  } elseif ( $recipient->isLoggedIn() ) {
1550  $recipient->setNewtalk( true, $legacyRevision );
1551  } else {
1552  wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
1553  }
1554  }
1555  }
1556  }
1557 
1558  if ( $title->getNamespace() == NS_MEDIAWIKI
1559  && $this->getRevisionSlotsUpdate()->isModifiedSlot( SlotRecord::MAIN )
1560  ) {
1561  $mainContent = $this->isContentDeleted() ? null : $this->getRawContent( SlotRecord::MAIN );
1562 
1563  $this->messageCache->updateMessageOverride( $title, $mainContent );
1564  }
1565 
1566  // TODO: move onArticleCreate and onArticle into a PageEventEmitter service
1567  if ( $this->options['created'] ) {
1569  } elseif ( $this->options['changed'] ) { // T52785
1570  WikiPage::onArticleEdit( $title, $legacyRevision, $this->getTouchedSlotRoles() );
1571  } elseif ( $this->options['restored'] ) {
1572  MediaWikiServices::getInstance()->getMainWANObjectCache()->touchCheckKey(
1573  "DerivedPageDataUpdater:restore:page:$id"
1574  );
1575  }
1576 
1577  $oldRevision = $this->getParentRevision();
1578  $oldLegacyRevision = $oldRevision ? new Revision( $oldRevision ) : null;
1579 
1580  // TODO: In the wiring, register a listener for this on the new PageEventEmitter
1582  $title,
1583  $oldLegacyRevision,
1584  $legacyRevision,
1585  $this->loadbalancerFactory->getLocalDomainID()
1586  );
1587 
1588  $this->doTransition( 'done' );
1589  }
1590 
1603  public function doSecondaryDataUpdates( array $options = [] ) {
1604  $this->assertHasRevision( __METHOD__ );
1605  $options += [ 'recursive' => false, 'defer' => false ];
1606  $deferValues = [ false, DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND ];
1607  if ( !in_array( $options['defer'], $deferValues, true ) ) {
1608  throw new InvalidArgumentException( 'Invalid value for defer: ' . $options['defer'] );
1609  }
1610  $updates = $this->getSecondaryDataUpdates( $options['recursive'] );
1611 
1612  $triggeringUser = $this->options['triggeringUser'] ?? $this->user;
1613  if ( !$triggeringUser instanceof User ) {
1614  $triggeringUser = User::newFromIdentity( $triggeringUser );
1615  }
1616  $causeAction = $this->options['causeAction'] ?? 'unknown';
1617  $causeAgent = $this->options['causeAgent'] ?? 'unknown';
1618  $legacyRevision = new Revision( $this->revision );
1619 
1620  foreach ( $updates as $update ) {
1621  if ( $update instanceof DataUpdate ) {
1622  $update->setCause( $causeAction, $causeAgent );
1623  }
1624  if ( $update instanceof LinksUpdate ) {
1625  $update->setRevision( $legacyRevision );
1626  $update->setTriggeringUser( $triggeringUser );
1627  }
1628  }
1629 
1630  if ( $options['defer'] === false ) {
1631  // T221577: flush any transaction; each update needs outer transaction scope
1632  $this->loadbalancerFactory->commitMasterChanges( __METHOD__ );
1633  foreach ( $updates as $update ) {
1634  DeferredUpdates::attemptUpdate( $update, $this->loadbalancerFactory );
1635  }
1636  } else {
1637  foreach ( $updates as $update ) {
1638  DeferredUpdates::addUpdate( $update, $options['defer'] );
1639  }
1640  }
1641  }
1642 
1643  public function doParserCacheUpdate() {
1644  $this->assertHasRevision( __METHOD__ );
1645 
1646  $wikiPage = $this->getWikiPage(); // TODO: ParserCache should accept a RevisionRecord instead
1647 
1648  // NOTE: this may trigger the first parsing of the new content after an edit (when not
1649  // using pre-generated stashed output).
1650  // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
1651  // to be performed post-send. The client could already follow a HTTP redirect to the
1652  // page view, but would then have to wait for a response until rendering is complete.
1653  $output = $this->getCanonicalParserOutput();
1654 
1655  // Save it to the parser cache. Use the revision timestamp in the case of a
1656  // freshly saved edit, as that matches page_touched and a mismatch would trigger an
1657  // unnecessary reparse.
1658  $timestamp = $this->options['newrev'] ? $this->revision->getTimestamp()
1659  : $output->getCacheTime();
1660  $this->parserCache->save(
1661  $output, $wikiPage, $this->getCanonicalParserOptions(),
1662  $timestamp, $this->revision->getId()
1663  );
1664  }
1665 
1666 }
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:1289
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:42
ContentHandler
A content handler knows how do deal with a specific type of content on a wiki page.
Definition: ContentHandler.php:55
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
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:254
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:125
MediaWiki\Storage\DerivedPageDataUpdater\$logger
LoggerInterface $logger
Definition: DerivedPageDataUpdater.php:145
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:3390
MediaWiki\Storage\DerivedPageDataUpdater\$parserCache
ParserCache $parserCache
Definition: DerivedPageDataUpdater.php:115
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:488
MediaWiki\Storage\DerivedPageDataUpdater\pageExisted
pageExisted()
Determines whether the page being edited already existed.
Definition: DerivedPageDataUpdater.php:455
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:470
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:890
MediaWiki\Storage\DerivedPageDataUpdater\$articleCountMethod
string $articleCountMethod
see $wgArticleCountMethod
Definition: DerivedPageDataUpdater.php:150
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:723
MediaWiki\Storage\DerivedPageDataUpdater\getSlotParserOutput
getSlotParserOutput( $role, $generateHtml=true)
Definition: DerivedPageDataUpdater.php:1279
MediaWiki\Storage\DerivedPageDataUpdater\$user
UserIdentity null $user
Definition: DerivedPageDataUpdater.php:105
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:130
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:1082
MediaWiki\Storage\DerivedPageDataUpdater\setArticleCountMethod
setArticleCountMethod( $articleCountMethod)
Definition: DerivedPageDataUpdater.php:420
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:79
MediaWiki\Storage\DerivedPageDataUpdater\getSlots
getSlots()
Returns the slots of the target revision, after PST.
Definition: DerivedPageDataUpdater.php:982
DeferredUpdates\attemptUpdate
static attemptUpdate(DeferrableUpdate $update, ILBFactory $lbFactory)
Attempt to run an update with the appropriate transaction round state it expects.
Definition: DeferredUpdates.php:393
true
return true
Definition: router.php:92
MediaWiki\Storage\DerivedPageDataUpdater\$parentRevision
RevisionRecord null $parentRevision
Definition: DerivedPageDataUpdater.php:208
MediaWiki\Storage\DerivedPageDataUpdater\setRcWatchCategoryMembership
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
Definition: DerivedPageDataUpdater.php:428
MediaWiki\Storage\DerivedPageDataUpdater\getPreparedEdit
getPreparedEdit()
Definition: DerivedPageDataUpdater.php:1253
Revision\MutableRevisionRecord\newFromParentRevision
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision,...
Definition: MutableRevisionRecord.php:53
ResourceLoaderWikiModule
Abstraction for ResourceLoader modules which pull from wiki pages.
Definition: ResourceLoaderWikiModule.php:54
Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
Definition: RevisionRecord.php:442
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Definition: DeferredUpdates.php:85
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:46
MediaWiki\Storage\DerivedPageDataUpdater\$revisionRenderer
RevisionRenderer $revisionRenderer
Definition: DerivedPageDataUpdater.php:223
LinksUpdate
Class the manages updates of *_link tables as well as similar extension-managed tables.
Definition: LinksUpdate.php:35
MediaWiki\Storage\DerivedPageDataUpdater\getPageId
getPageId()
Definition: DerivedPageDataUpdater.php:563
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:537
Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:191
WikiPage\getRevision
getRevision()
Get the latest revision.
Definition: WikiPage.php:786
MediaWiki\Storage\DerivedPageDataUpdater\$options
$options
Stores (most of) the $options parameter of prepareUpdate().
Definition: DerivedPageDataUpdater.php:161
MediaWiki\Storage\DerivedPageDataUpdater\$renderedRevision
RenderedRevision $renderedRevision
Definition: DerivedPageDataUpdater.php:218
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:593
MediaWiki\Storage\DerivedPageDataUpdater\$slotsUpdate
RevisionSlotsUpdate null $slotsUpdate
Definition: DerivedPageDataUpdater.php:203
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
Title\equals
equals(LinkTarget $title)
Compare with another title.
Definition: Title.php:4103
WikiPage\onArticleEdit
static onArticleEdit(Title $title, Revision $revision=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3483
MediaWiki\Storage\DerivedPageDataUpdater\isRedirect
isRedirect()
Definition: DerivedPageDataUpdater.php:679
MediaWiki\Storage\DerivedPageDataUpdater\getCanonicalParserOptions
getCanonicalParserOptions()
Definition: DerivedPageDataUpdater.php:1296
MediaWiki\Storage\DerivedPageDataUpdater\doUpdates
doUpdates()
Do standard updates after page edit, purge, or import.
Definition: DerivedPageDataUpdater.php:1412
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
DataUpdate
Abstract base class for update jobs that do something with some secondary data extracted from article...
Definition: DataUpdate.php:28
MediaWiki\Storage\DerivedPageDataUpdater\$revisionStore
RevisionStore $revisionStore
Definition: DerivedPageDataUpdater.php:120
MediaWiki\Storage\DerivedPageDataUpdater\assertHasRevision
assertHasRevision( $method)
Definition: DerivedPageDataUpdater.php:921
Revision
Definition: Revision.php:40
MediaWiki\MediaWikiServices\getInstance
static getInstance()
Returns the global default instance of the top level service locator.
Definition: MediaWikiServices.php:161
MediaWiki\Storage\DerivedPageDataUpdater\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: DerivedPageDataUpdater.php:226
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:357
MediaWiki\Storage\DerivedPageDataUpdater\setLogger
setLogger(LoggerInterface $logger)
Definition: DerivedPageDataUpdater.php:307
ParserOptions\newFromUserAndLang
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
Definition: ParserOptions.php:1040
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:992
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:621
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:409
MediaWiki\Storage\DerivedPageDataUpdater\assertPrepared
assertPrepared( $method)
Definition: DerivedPageDataUpdater.php:913
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:613
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:70
Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:60
MediaWiki\User\UserIdentity\getName
getName()
$title
$title
Definition: testCompression.php:36
MediaWiki\Storage\DerivedPageDataUpdater\wasRedirect
wasRedirect()
Whether the page was a redirect before the edit.
Definition: DerivedPageDataUpdater.php:958
SiteStatsUpdate\factory
static factory(array $deltas)
Definition: SiteStatsUpdate.php:71
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1900
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:934
MediaWiki\Storage\DerivedPageDataUpdater\assertTransition
assertTransition( $newStage)
Asserts that a transition to the given stage is possible, without performing it.
Definition: DerivedPageDataUpdater.php:340
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:913
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:945
MediaWiki\Storage\DerivedPageDataUpdater\useMaster
useMaster()
Definition: DerivedPageDataUpdater.php:626
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:38
ResourceLoaderWikiModule\invalidateModuleCache
static invalidateModuleCache(Title $title, ?Revision $old, ?Revision $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:540
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:78
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:63
MWCallableUpdate\setTransactionRoundRequirement
setTransactionRoundRequirement( $mode)
Definition: MWCallableUpdate.php:60
MediaWiki\Storage\DerivedPageDataUpdater\isContentDeleted
isContentDeleted()
Whether the content is deleted and thus not visible to the public.
Definition: DerivedPageDataUpdater.php:573
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:42
MediaWiki\Storage\DerivedPageDataUpdater\getWikiPage
getWikiPage()
Definition: DerivedPageDataUpdater.php:443
MediaWiki\Storage\DerivedPageDataUpdater\isCountable
isCountable()
Definition: DerivedPageDataUpdater.php:634
MediaWiki\Storage\DerivedPageDataUpdater\isContentPrepared
isContentPrepared()
Whether prepareUpdate() or prepareContent() have been called on this instance.
Definition: DerivedPageDataUpdater.php:545
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:140
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:1030
MediaWiki\Storage\DerivedPageDataUpdater\$pageState
array $pageState
The state of the relevant row in page table before the edit.
Definition: DerivedPageDataUpdater.php:198
MediaWiki\Storage
Definition: BlobAccessException.php:23
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:41
MediaWiki\Storage\DerivedPageDataUpdater\$messageCache
MessageCache $messageCache
Definition: DerivedPageDataUpdater.php:135
MediaWiki\Storage\DerivedPageDataUpdater\doTransition
doTransition( $newStage)
Transition function for managing the life cycle of this instances.
Definition: DerivedPageDataUpdater.php:322
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:1021
Content
Base interface for content objects.
Definition: Content.php:34
MWCallableUpdate
Deferrable Update for closure/callback.
Definition: MWCallableUpdate.php:8
MediaWiki\Storage\DerivedPageDataUpdater\$jobQueueGroup
JobQueueGroup $jobQueueGroup
Definition: DerivedPageDataUpdater.php:130
Revision\RevisionRecord\getVisibility
getVisibility()
Get the deletion bitfield of the revision.
Definition: RevisionRecord.php:431
Title
Represents a title within MediaWiki.
Definition: Title.php:42
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:603
DeferredUpdates\PRESEND
const PRESEND
Definition: DeferredUpdates.php:69
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:435
MediaWiki\Storage\DerivedPageDataUpdater\__construct
__construct(WikiPage $wikiPage, RevisionStore $revisionStore, RevisionRenderer $revisionRenderer, SlotRoleRegistry $slotRoleRegistry, ParserCache $parserCache, JobQueueGroup $jobQueueGroup, MessageCache $messageCache, Language $contLang, ILBFactory $loadbalancerFactory)
Definition: DerivedPageDataUpdater.php:281
MediaWiki\Storage\DerivedPageDataUpdater\getSecondaryDataUpdates
getSecondaryDataUpdates( $recursive=false)
Definition: DerivedPageDataUpdater.php:1305
MediaWiki\Storage\DerivedPageDataUpdater\doParserCacheUpdate
doParserCacheUpdate()
Definition: DerivedPageDataUpdater.php:1643
Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:49
MediaWiki\Storage\DerivedPageDataUpdater\getRenderedRevision
getRenderedRevision()
Definition: DerivedPageDataUpdater.php:898
Revision\RevisionSlots
Value object representing the set of slots belonging to a revision.
Definition: RevisionSlots.php:39
MediaWiki\Storage\DerivedPageDataUpdater\$rcWatchCategoryMembership
boolean $rcWatchCategoryMembership
see $wgRCWatchCategoryMembership
Definition: DerivedPageDataUpdater.php:155
ParserCache
Definition: ParserCache.php:30
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:213
MediaWiki\Storage\DerivedPageDataUpdater\getRawSlot
getRawSlot( $role)
Returns the slot, modified or inherited, after PST, with no audience checks applied.
Definition: DerivedPageDataUpdater.php:591
Revision\SlotRoleRegistry
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Definition: SlotRoleRegistry.php:48
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 (such as updating link tables).
Definition: DerivedPageDataUpdater.php:1603
Revision\RenderedRevision
RenderedRevision represents the rendered representation of a revision.
Definition: RenderedRevision.php:44
CategoryMembershipChangeJob\newSpec
static newSpec(Title $title, $revisionTimestamp)
Definition: CategoryMembershipChangeJob.php:54
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:948
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:68
MediaWiki\Storage\DerivedPageDataUpdater\assertHasPageState
assertHasPageState( $method)
Definition: DerivedPageDataUpdater.php:904
DeferrableUpdate
Interface that deferrable updates should implement.
Definition: DeferrableUpdate.php:9
MediaWiki\Storage\DerivedPageDataUpdater\getTouchedSlotRoles
getTouchedSlotRoles()
Returns the role names of the slots touched by the new revision, including removed roles.
Definition: DerivedPageDataUpdater.php:1011
Revision\getRevisionRecord
getRevisionRecord()
Definition: Revision.php:433
MediaWiki\Storage\DerivedPageDataUpdater\$stage
string $stage
A stage identifier for managing the life cycle of this instance.
Definition: DerivedPageDataUpdater.php:236
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:633
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:100
MediaWiki\Storage\DerivedPageDataUpdater\$wikiPage
WikiPage $wikiPage
Definition: DerivedPageDataUpdater.php:110
MessageCache
Cache of messages that are defined by MediaWiki namespace pages or by hooks.
Definition: MessageCache.php:43
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:52
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:125
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
MediaWiki\Storage\DerivedPageDataUpdater\revisionIsRedirect
revisionIsRedirect(RevisionRecord $rev)
Definition: DerivedPageDataUpdater.php:692
Language
Internationalisation code.
Definition: Language.php:39
MediaWiki\Storage\DerivedPageDataUpdater\grabCurrentRevision
grabCurrentRevision()
Returns the revision that was the page's current revision when grabCurrentRevision() was first called...
Definition: DerivedPageDataUpdater.php:512
MediaWiki\Storage\DerivedPageDataUpdater\isUpdatePrepared
isUpdatePrepared()
Whether prepareUpdate() has been called on this instance.
Definition: DerivedPageDataUpdater.php:556
Hooks
Hooks class.
Definition: Hooks.php:34
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:1027
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