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;
63 use Revision;
64 use SearchUpdate;
65 use SiteStatsUpdate;
66 use Title;
67 use User;
68 use Wikimedia\Assert\Assert;
70 use WikiPage;
71 
103 class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface {
104 
108  private $user = null;
109 
113  private $wikiPage;
114 
118  private $parserCache;
119 
123  private $revisionStore;
124 
128  private $contLang;
129 
133  private $jobQueueGroup;
134 
138  private $messageCache;
139 
144 
148  private $hookRunner;
149 
153  private $logger;
154 
159 
163  private $rcWatchCategoryMembership = false;
164 
169  private $options = [
170  'changed' => true,
171  // newrev is true if prepareUpdate is handling the creation of a new revision,
172  // as opposed to a null edit or a forced update.
173  'newrev' => false,
174  'created' => false,
175  'moved' => false,
176  'restored' => false,
177  'oldrevision' => null,
178  'oldcountable' => null,
179  'oldredirect' => null,
180  'triggeringUser' => null,
181  // causeAction/causeAgent default to 'unknown' but that's handled where it's read,
182  // to make the life of prepareUpdate() callers easier.
183  'causeAction' => null,
184  'causeAgent' => null,
185  ];
186 
206  private $pageState = null;
207 
211  private $slotsUpdate = null;
212 
216  private $parentRevision = null;
217 
221  private $revision = null;
222 
226  private $renderedRevision = null;
227 
232 
235 
244  private $stage = 'new';
245 
256  private const TRANSITIONS = [
257  'new' => [
258  'new' => true,
259  'knows-current' => true,
260  'has-content' => true,
261  'has-revision' => true,
262  ],
263  'knows-current' => [
264  'knows-current' => true,
265  'has-content' => true,
266  'has-revision' => true,
267  ],
268  'has-content' => [
269  'has-content' => true,
270  'has-revision' => true,
271  ],
272  'has-revision' => [
273  'has-revision' => true,
274  'done' => true,
275  ],
276  ];
277 
282 
296  public function __construct(
307  HookContainer $hookContainer
308  ) {
309  $this->wikiPage = $wikiPage;
310 
311  $this->parserCache = $parserCache;
312  $this->revisionStore = $revisionStore;
313  $this->revisionRenderer = $revisionRenderer;
314  $this->slotRoleRegistry = $slotRoleRegistry;
315  $this->jobQueueGroup = $jobQueueGroup;
316  $this->messageCache = $messageCache;
317  $this->contLang = $contLang;
318  // XXX only needed for waiting for replicas to catch up; there should be a narrower
319  // interface for that.
320  $this->loadbalancerFactory = $loadbalancerFactory;
321  $this->contentHandlerFactory = $contentHandlerFactory;
322  $this->hookRunner = new HookRunner( $hookContainer );
323 
324  $this->logger = new NullLogger();
325  }
326 
327  public function setLogger( LoggerInterface $logger ) {
328  $this->logger = $logger;
329  }
330 
342  private function doTransition( $newStage ) {
343  $this->assertTransition( $newStage );
344 
345  $oldStage = $this->stage;
346  $this->stage = $newStage;
347 
348  return $oldStage;
349  }
350 
360  private function assertTransition( $newStage ) {
361  if ( empty( self::TRANSITIONS[$this->stage][$newStage] ) ) {
362  throw new LogicException( "Cannot transition from {$this->stage} to $newStage" );
363  }
364  }
365 
377  public function isReusableFor(
378  UserIdentity $user = null,
379  RevisionRecord $revision = null,
381  $parentId = null
382  ) {
383  if ( $revision
384  && $parentId
385  && $revision->getParentId() !== $parentId
386  ) {
387  throw new InvalidArgumentException( '$parentId should match the parent of $revision' );
388  }
389 
390  // NOTE: For null revisions, $user may be different from $this->revision->getUser
391  // and also from $revision->getUser.
392  // But $user should always match $this->user.
393  if ( $user && $this->user && $user->getName() !== $this->user->getName() ) {
394  return false;
395  }
396 
397  if ( $revision && $this->revision && $this->revision->getId()
398  && $this->revision->getId() !== $revision->getId()
399  ) {
400  return false;
401  }
402 
403  if ( $this->pageState
404  && $revision
405  && $revision->getParentId() !== null
406  && $this->pageState['oldId'] !== $revision->getParentId()
407  ) {
408  return false;
409  }
410 
411  if ( $this->pageState
412  && $parentId !== null
413  && $this->pageState['oldId'] !== $parentId
414  ) {
415  return false;
416  }
417 
418  // NOTE: this check is the primary reason for having the $this->slotsUpdate field!
419  if ( $this->slotsUpdate
420  && $slotsUpdate
421  && !$this->slotsUpdate->hasSameUpdates( $slotsUpdate )
422  ) {
423  return false;
424  }
425 
426  if ( $revision
427  && $this->revision
428  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
429  ) {
430  return false;
431  }
432 
433  return true;
434  }
435 
441  $this->articleCountMethod = $articleCountMethod;
442  }
443 
449  $this->rcWatchCategoryMembership = $rcWatchCategoryMembership;
450  }
451 
455  private function getTitle() {
456  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
457  return $this->wikiPage->getTitle();
458  }
459 
463  private function getWikiPage() {
464  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
465  return $this->wikiPage;
466  }
467 
475  public function pageExisted() {
476  $this->assertHasPageState( __METHOD__ );
477 
478  return $this->pageState['oldId'] > 0;
479  }
480 
490  private function getParentRevision() {
491  $this->assertPrepared( __METHOD__ );
492 
493  if ( $this->parentRevision ) {
494  return $this->parentRevision;
495  }
496 
497  if ( !$this->pageState['oldId'] ) {
498  // If there was no current revision, there is no parent revision,
499  // since the page didn't exist.
500  return null;
501  }
502 
503  $oldId = $this->revision->getParentId();
504  $flags = $this->useMaster() ? RevisionStore::READ_LATEST : 0;
505  $this->parentRevision = $oldId
506  ? $this->revisionStore->getRevisionById( $oldId, $flags )
507  : null;
508 
509  return $this->parentRevision;
510  }
511 
532  public function grabCurrentRevision() {
533  if ( $this->pageState ) {
534  return $this->pageState['oldRevision'];
535  }
536 
537  $this->assertTransition( 'knows-current' );
538 
539  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
540  $wikiPage = $this->getWikiPage();
541 
542  // Do not call WikiPage::clear(), since the caller may already have caused page data
543  // to be loaded with SELECT FOR UPDATE. Just assert it's loaded now.
544  $wikiPage->loadPageData( self::READ_LATEST );
545  $current = $wikiPage->getRevisionRecord();
546 
547  $this->pageState = [
548  'oldRevision' => $current,
549  'oldId' => $current ? $current->getId() : 0,
550  'oldIsRedirect' => $wikiPage->isRedirect(), // NOTE: uses page table
551  'oldCountable' => $wikiPage->isCountable(), // NOTE: uses pagelinks table
552  ];
553 
554  $this->doTransition( 'knows-current' );
555 
556  return $this->pageState['oldRevision'];
557  }
558 
564  public function isContentPrepared() {
565  return $this->revision !== null;
566  }
567 
575  public function isUpdatePrepared() {
576  return $this->revision !== null && $this->revision->getId() !== null;
577  }
578 
582  private function getPageId() {
583  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
584  return $this->wikiPage->getId();
585  }
586 
592  public function isContentDeleted() {
593  if ( $this->revision ) {
594  return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
595  } else {
596  // If the content has not been saved yet, it cannot have been deleted yet.
597  return false;
598  }
599  }
600 
610  public function getRawSlot( $role ) {
611  return $this->getSlots()->getSlot( $role );
612  }
613 
622  public function getRawContent( $role ) {
623  return $this->getRawSlot( $role )->getContent();
624  }
625 
632  private function getContentModel( $role ) {
633  return $this->getRawSlot( $role )->getModel();
634  }
635 
641  private function getContentHandler( $role ): ContentHandler {
642  return $this->contentHandlerFactory
643  ->getContentHandler( $this->getContentModel( $role ) );
644  }
645 
646  private function useMaster() {
647  // TODO: can we just set a flag to true in prepareContent()?
648  return $this->wikiPage->wasLoadedFrom( self::READ_LATEST );
649  }
650 
654  public function isCountable() {
655  // NOTE: Keep in sync with WikiPage::isCountable.
656 
657  if ( !$this->getTitle()->isContentPage() ) {
658  return false;
659  }
660 
661  if ( $this->isContentDeleted() ) {
662  // This should be irrelevant: countability only applies to the current revision,
663  // and the current revision is never suppressed.
664  return false;
665  }
666 
667  if ( $this->isRedirect() ) {
668  return false;
669  }
670 
671  $hasLinks = null;
672 
673  if ( $this->articleCountMethod === 'link' ) {
674  // NOTE: it would be more appropriate to determine for each slot separately
675  // whether it has links, and use that information with that slot's
676  // isCountable() method. However, that would break parity with
677  // WikiPage::isCountable, which uses the pagelinks table to determine
678  // whether the current revision has links.
679  $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() );
680  }
681 
682  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
683  $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
684  if ( $roleHandler->supportsArticleCount() ) {
685  $content = $this->getRawContent( $role );
686 
687  if ( $content->isCountable( $hasLinks ) ) {
688  return true;
689  }
690  }
691  }
692 
693  return false;
694  }
695 
699  public function isRedirect() {
700  // NOTE: main slot determines redirect status
701  // TODO: MCR: this should be controlled by a PageTypeHandler
702  $mainContent = $this->getRawContent( SlotRecord::MAIN );
703 
704  return $mainContent->isRedirect();
705  }
706 
712  private function revisionIsRedirect( RevisionRecord $rev ) {
713  // NOTE: main slot determines redirect status
714  $mainContent = $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
715 
716  return $mainContent->isRedirect();
717  }
718 
743  public function prepareContent(
744  User $user,
746  $useStash = true
747  ) {
748  if ( $this->slotsUpdate ) {
749  if ( !$this->user ) {
750  throw new LogicException(
751  'Unexpected state: $this->slotsUpdate was initialized, '
752  . 'but $this->user was not.'
753  );
754  }
755 
756  if ( $this->user->getName() !== $user->getName() ) {
757  throw new LogicException( 'Can\'t call prepareContent() again for different user! '
758  . 'Expected ' . $this->user->getName() . ', got ' . $user->getName()
759  );
760  }
761 
762  if ( !$this->slotsUpdate->hasSameUpdates( $slotsUpdate ) ) {
763  throw new LogicException(
764  'Can\'t call prepareContent() again with different slot content!'
765  );
766  }
767 
768  return; // prepareContent() already done, nothing to do
769  }
770 
771  $this->assertTransition( 'has-content' );
772 
773  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
774  $title = $this->getTitle();
775 
777 
778  // The edit may have already been prepared via api.php?action=stashedit
779  $stashedEdit = false;
780 
781  // TODO: MCR: allow output for all slots to be stashed.
782  if ( $useStash && $slotsUpdate->isModifiedSlot( SlotRecord::MAIN ) ) {
783  $editStash = MediaWikiServices::getInstance()->getPageEditStash();
784  $stashedEdit = $editStash->checkCache(
785  $title,
788  );
789  }
790 
791  $userPopts = ParserOptions::newFromUserAndLang( $user, $this->contLang );
792  $this->hookRunner->onArticlePrepareTextForEdit( $wikiPage, $userPopts );
793 
794  $this->user = $user;
795  $this->slotsUpdate = $slotsUpdate;
796 
797  if ( $parentRevision ) {
799  } else {
800  $this->revision = new MutableRevisionRecord( $title );
801  }
802 
803  // NOTE: user and timestamp must be set, so they can be used for
804  // {{subst:REVISIONUSER}} and {{subst:REVISIONTIMESTAMP}} in PST!
805  $this->revision->setTimestamp( MWTimestamp::now( TS_MW ) );
806  $this->revision->setUser( $user );
807 
808  // Set up ParserOptions to operate on the new revision
809  $oldCallback = $userPopts->getCurrentRevisionRecordCallback();
810  $userPopts->setCurrentRevisionRecordCallback(
811  function ( Title $parserTitle, $parser = null ) use ( $title, $oldCallback ) {
812  if ( $parserTitle->equals( $title ) ) {
813  return $this->revision;
814  } else {
815  return call_user_func( $oldCallback, $parserTitle, $parser );
816  }
817  }
818  );
819 
820  $pstContentSlots = $this->revision->getSlots();
821 
822  foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
823  $slot = $slotsUpdate->getModifiedSlot( $role );
824 
825  if ( $slot->isInherited() ) {
826  // No PST for inherited slots! Note that "modified" slots may still be inherited
827  // from an earlier version, e.g. for rollbacks.
828  $pstSlot = $slot;
829  } elseif ( $role === SlotRecord::MAIN && $stashedEdit ) {
830  // TODO: MCR: allow PST content for all slots to be stashed.
831  $pstSlot = SlotRecord::newUnsaved( $role, $stashedEdit->pstContent );
832  } else {
833  $content = $slot->getContent();
834  $pstContent = $content->preSaveTransform( $title, $this->user, $userPopts );
835  $pstSlot = SlotRecord::newUnsaved( $role, $pstContent );
836  }
837 
838  $pstContentSlots->setSlot( $pstSlot );
839  }
840 
841  foreach ( $slotsUpdate->getRemovedRoles() as $role ) {
842  $pstContentSlots->removeSlot( $role );
843  }
844 
845  $this->options['created'] = ( $parentRevision === null );
846  $this->options['changed'] = ( $parentRevision === null
847  || !$pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
848 
849  $this->doTransition( 'has-content' );
850 
851  if ( !$this->options['changed'] ) {
852  // null-edit!
853 
854  // TODO: move this into MutableRevisionRecord
855  // TODO: This needs to behave differently for a forced dummy edit!
856  $this->revision->setId( $parentRevision->getId() );
857  $this->revision->setTimestamp( $parentRevision->getTimestamp() );
858  $this->revision->setPageId( $parentRevision->getPageId() );
859  $this->revision->setParentId( $parentRevision->getParentId() );
860  $this->revision->setUser( $parentRevision->getUser( RevisionRecord::RAW ) );
861  $this->revision->setComment( $parentRevision->getComment( RevisionRecord::RAW ) );
862  $this->revision->setMinorEdit( $parentRevision->isMinor() );
863  $this->revision->setVisibility( $parentRevision->getVisibility() );
864 
865  // prepareUpdate() is redundant for null-edits
866  $this->doTransition( 'has-revision' );
867  } else {
868  $this->parentRevision = $parentRevision;
869  }
870 
871  $renderHints = [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ];
872 
873  if ( $stashedEdit ) {
875  $output = $stashedEdit->output;
876  // TODO: this should happen when stashing the ParserOutput, not now!
877  $output->setCacheTime( $stashedEdit->timestamp );
878 
879  $renderHints['known-revision-output'] = $output;
880 
881  $this->logger->debug( __METHOD__ . ': using stashed edit output...' );
882  }
883 
884  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
885  // NOTE: the revision is either new or current, so we can bypass audience checks.
886  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
887  $this->revision,
888  null,
889  null,
890  $renderHints
891  );
892  }
893 
909  public function getRevision() {
910  $this->assertPrepared( __METHOD__ );
911  return $this->revision;
912  }
913 
917  public function getRenderedRevision() {
918  $this->assertPrepared( __METHOD__ );
919 
921  }
922 
923  private function assertHasPageState( $method ) {
924  if ( !$this->pageState ) {
925  throw new LogicException(
926  'Must call grabCurrentRevision() or prepareContent() '
927  . 'or prepareUpdate() before calling ' . $method
928  );
929  }
930  }
931 
932  private function assertPrepared( $method ) {
933  if ( !$this->revision ) {
934  throw new LogicException(
935  'Must call prepareContent() or prepareUpdate() before calling ' . $method
936  );
937  }
938  }
939 
940  private function assertHasRevision( $method ) {
941  if ( !$this->revision->getId() ) {
942  throw new LogicException(
943  'Must call prepareUpdate() before calling ' . $method
944  );
945  }
946  }
947 
953  public function isCreation() {
954  $this->assertPrepared( __METHOD__ );
955  return $this->options['created'];
956  }
957 
967  public function isChange() {
968  $this->assertPrepared( __METHOD__ );
969  return $this->options['changed'];
970  }
971 
977  public function wasRedirect() {
978  $this->assertHasPageState( __METHOD__ );
979 
980  if ( $this->pageState['oldIsRedirect'] === null ) {
982  $rev = $this->pageState['oldRevision'];
983  if ( $rev ) {
984  $this->pageState['oldIsRedirect'] = $this->revisionIsRedirect( $rev );
985  } else {
986  $this->pageState['oldIsRedirect'] = false;
987  }
988  }
989 
990  return $this->pageState['oldIsRedirect'];
991  }
992 
1001  public function getSlots() {
1002  $this->assertPrepared( __METHOD__ );
1003  return $this->revision->getSlots();
1004  }
1005 
1011  private function getRevisionSlotsUpdate() {
1012  $this->assertPrepared( __METHOD__ );
1013 
1014  if ( !$this->slotsUpdate ) {
1015  $old = $this->getParentRevision();
1016  $this->slotsUpdate = RevisionSlotsUpdate::newFromRevisionSlots(
1017  $this->revision->getSlots(),
1018  $old ? $old->getSlots() : null
1019  );
1020  }
1021  return $this->slotsUpdate;
1022  }
1023 
1030  public function getTouchedSlotRoles() {
1031  return $this->getRevisionSlotsUpdate()->getTouchedRoles();
1032  }
1033 
1040  public function getModifiedSlotRoles() {
1041  return $this->getRevisionSlotsUpdate()->getModifiedRoles();
1042  }
1043 
1049  public function getRemovedSlotRoles() {
1050  return $this->getRevisionSlotsUpdate()->getRemovedRoles();
1051  }
1052 
1102  public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
1103  if ( isset( $options['oldrevision'] ) && $options['oldrevision'] instanceof Revision ) {
1104  wfDeprecated(
1105  __METHOD__ . ' with the `oldrevision` option being a ' .
1106  'Revision object',
1107  '1.35'
1108  );
1109  $options['oldrevision'] = $options['oldrevision']->getRevisionRecord();
1110  }
1111  Assert::parameter(
1112  !isset( $options['oldrevision'] )
1113  || $options['oldrevision'] instanceof RevisionRecord,
1114  '$options["oldrevision"]',
1115  'must be a RevisionRecord (or Revision)'
1116  );
1117  Assert::parameter(
1118  !isset( $options['triggeringUser'] )
1119  || $options['triggeringUser'] instanceof UserIdentity,
1120  '$options["triggeringUser"]',
1121  'must be a UserIdentity'
1122  );
1123 
1124  if ( !$revision->getId() ) {
1125  throw new InvalidArgumentException(
1126  'Revision must have an ID set for it to be used with prepareUpdate()!'
1127  );
1128  }
1129 
1130  if ( $this->revision && $this->revision->getId() ) {
1131  if ( $this->revision->getId() === $revision->getId() ) {
1132  return; // nothing to do!
1133  } else {
1134  throw new LogicException(
1135  'Trying to re-use DerivedPageDataUpdater with revision '
1136  . $revision->getId()
1137  . ', but it\'s already bound to revision '
1138  . $this->revision->getId()
1139  );
1140  }
1141  }
1142 
1143  if ( $this->revision
1144  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
1145  ) {
1146  throw new LogicException(
1147  'The Revision provided has mismatching content!'
1148  );
1149  }
1150 
1151  // Override fields defined in $this->options with values from $options.
1152  $this->options = array_intersect_key( $options, $this->options ) + $this->options;
1153 
1154  if ( $this->revision ) {
1155  $oldId = $this->pageState['oldId'] ?? 0;
1156  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1157  } elseif ( isset( $this->options['oldrevision'] ) ) {
1159  $oldRev = $this->options['oldrevision'];
1160  $oldId = $oldRev->getId();
1161  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1162  } else {
1163  $oldId = $revision->getParentId();
1164  }
1165 
1166  if ( $oldId !== null ) {
1167  // XXX: what if $options['changed'] disagrees?
1168  // MovePage creates a dummy revision with changed = false!
1169  // We may want to explicitly distinguish between "no new revision" (null-edit)
1170  // and "new revision without new content" (dummy revision).
1171 
1172  if ( $oldId === $revision->getParentId() ) {
1173  // NOTE: this may still be a NullRevision!
1174  // New revision!
1175  $this->options['changed'] = true;
1176  } elseif ( $oldId === $revision->getId() ) {
1177  // Null-edit!
1178  $this->options['changed'] = false;
1179  } else {
1180  // This indicates that calling code has given us the wrong Revision object
1181  throw new LogicException(
1182  'The Revision mismatches old revision ID: '
1183  . 'Old ID is ' . $oldId
1184  . ', parent ID is ' . $revision->getParentId()
1185  . ', revision ID is ' . $revision->getId()
1186  );
1187  }
1188  }
1189 
1190  // If prepareContent() was used to generate the PST content (which is indicated by
1191  // $this->slotsUpdate being set), and this is not a null-edit, then the given
1192  // revision must have the acting user as the revision author. Otherwise, user
1193  // signatures generated by PST would mismatch the user in the revision record.
1194  if ( $this->user !== null && $this->options['changed'] && $this->slotsUpdate ) {
1195  $user = $revision->getUser();
1196  if ( !$this->user->equals( $user ) ) {
1197  throw new LogicException(
1198  'The Revision provided has a mismatching actor: expected '
1199  . $this->user->getName()
1200  . ', got '
1201  . $user->getName()
1202  );
1203  }
1204  }
1205 
1206  // If $this->pageState was not yet initialized by grabCurrentRevision or prepareContent,
1207  // emulate the state of the page table before the edit, as good as we can.
1208  if ( !$this->pageState ) {
1209  $this->pageState = [
1210  'oldIsRedirect' => isset( $this->options['oldredirect'] )
1211  && is_bool( $this->options['oldredirect'] )
1212  ? $this->options['oldredirect']
1213  : null,
1214  'oldCountable' => isset( $this->options['oldcountable'] )
1215  && is_bool( $this->options['oldcountable'] )
1216  ? $this->options['oldcountable']
1217  : null,
1218  ];
1219 
1220  if ( $this->options['changed'] ) {
1221  // The edit created a new revision
1222  $this->pageState['oldId'] = $revision->getParentId();
1223 
1224  if ( isset( $this->options['oldrevision'] ) ) {
1225  $rev = $this->options['oldrevision'];
1226  $this->pageState['oldRevision'] = $rev;
1227  }
1228  } else {
1229  // This is a null-edit, so the old revision IS the new revision!
1230  $this->pageState['oldId'] = $revision->getId();
1231  $this->pageState['oldRevision'] = $revision;
1232  }
1233  }
1234 
1235  // "created" is forced here
1236  $this->options['created'] = ( $this->options['created'] ||
1237  ( $this->pageState['oldId'] === 0 ) );
1238 
1239  $this->revision = $revision;
1240 
1241  $this->doTransition( 'has-revision' );
1242 
1243  // NOTE: in case we have a User object, don't override with a UserIdentity.
1244  // We already checked that $revision->getUser() mathces $this->user;
1245  if ( !$this->user ) {
1246  $this->user = $revision->getUser( RevisionRecord::RAW );
1247  }
1248 
1249  // Prune any output that depends on the revision ID.
1250  if ( $this->renderedRevision ) {
1251  $this->renderedRevision->updateRevision( $revision );
1252  } else {
1253  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
1254  // NOTE: the revision is either new or current, so we can bypass audience checks.
1255  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
1256  $this->revision,
1257  null,
1258  null,
1259  [
1260  'use-master' => $this->useMaster(),
1261  'audience' => RevisionRecord::RAW,
1262  'known-revision-output' => $options['known-revision-output'] ?? null
1263  ]
1264  );
1265 
1266  // XXX: Since we presumably are dealing with the current revision,
1267  // we could try to get the ParserOutput from the parser cache.
1268  }
1269 
1270  // TODO: optionally get ParserOutput from the ParserCache here.
1271  // Move the logic used by RefreshLinksJob here!
1272  }
1273 
1278  public function getPreparedEdit() {
1279  $this->assertPrepared( __METHOD__ );
1280 
1282  $preparedEdit = new PreparedEdit();
1283 
1284  $preparedEdit->popts = $this->getCanonicalParserOptions();
1285  $preparedEdit->parserOutputCallback = [ $this, 'getCanonicalParserOutput' ];
1286  $preparedEdit->pstContent = $this->revision->getContent( SlotRecord::MAIN );
1287  $preparedEdit->newContent =
1289  ? $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent()
1290  : $this->revision->getContent( SlotRecord::MAIN ); // XXX: can we just remove this?
1291  $preparedEdit->oldContent = null; // unused. // XXX: could get this from the parent revision
1292  $preparedEdit->revid = $this->revision ? $this->revision->getId() : null;
1293  $preparedEdit->timestamp = $preparedEdit->output->getCacheTime();
1294  $preparedEdit->format = $preparedEdit->pstContent->getDefaultFormat();
1295 
1296  return $preparedEdit;
1297  }
1298 
1304  public function getSlotParserOutput( $role, $generateHtml = true ) {
1305  return $this->getRenderedRevision()->getSlotParserOutput(
1306  $role,
1307  [ 'generate-html' => $generateHtml ]
1308  );
1309  }
1310 
1314  public function getCanonicalParserOutput() {
1315  return $this->getRenderedRevision()->getRevisionParserOutput();
1316  }
1317 
1321  public function getCanonicalParserOptions() {
1322  return $this->getRenderedRevision()->getOptions();
1323  }
1324 
1330  public function getSecondaryDataUpdates( $recursive = false ) {
1331  if ( $this->isContentDeleted() ) {
1332  // This shouldn't happen, since the current content is always public,
1333  // and DataUpates are only needed for current content.
1334  return [];
1335  }
1336 
1337  $output = $this->getCanonicalParserOutput();
1338 
1339  // Construct a LinksUpdate for the combined canonical output.
1340  $linksUpdate = new LinksUpdate(
1341  $this->getTitle(),
1342  $output,
1343  $recursive
1344  );
1345 
1346  $allUpdates = [ $linksUpdate ];
1347 
1348  // NOTE: Run updates for all slots, not just the modified slots! Otherwise,
1349  // info for an inherited slot may end up being removed. This is also needed
1350  // to ensure that purges are effective.
1352  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
1353  $slot = $this->getRawSlot( $role );
1354  $content = $slot->getContent();
1355  $handler = $content->getContentHandler();
1356 
1357  $updates = $handler->getSecondaryDataUpdates(
1358  $this->getTitle(),
1359  $content,
1360  $role,
1362  );
1363  $allUpdates = array_merge( $allUpdates, $updates );
1364 
1365  // TODO: remove B/C hack in 1.32!
1366  // NOTE: we assume that the combined output contains all relevant meta-data for
1367  // all slots!
1368  $legacyUpdates = $content->getSecondaryDataUpdates(
1369  $this->getTitle(),
1370  null,
1371  $recursive,
1372  $output
1373  );
1374 
1375  // HACK: filter out redundant and incomplete LinksUpdates
1376  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1377  return !( $update instanceof LinksUpdate );
1378  } );
1379 
1380  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1381  }
1382 
1383  // XXX: if a slot was removed by an earlier edit, but deletion updates failed to run at
1384  // that time, we don't know for which slots to run deletion updates when purging a page.
1385  // We'd have to examine the entire history of the page to determine that. Perhaps there
1386  // could be a "try extra hard" mode for that case that would run a DB query to find all
1387  // roles/models ever used on the page. On the other hand, removing slots should be quite
1388  // rare, so perhaps this isn't worth the trouble.
1389 
1390  // TODO: consolidate with similar logic in WikiPage::getDeletionUpdates()
1391  $wikiPage = $this->getWikiPage();
1393  foreach ( $this->getRemovedSlotRoles() as $role ) {
1394  // HACK: we should get the content model of the removed slot from a SlotRoleHandler!
1395  // For now, find the slot in the parent revision - if the slot was removed, it should
1396  // always exist in the parent revision.
1397  $parentSlot = $parentRevision->getSlot( $role, RevisionRecord::RAW );
1398  $content = $parentSlot->getContent();
1399  $handler = $content->getContentHandler();
1400 
1401  $updates = $handler->getDeletionUpdates(
1402  $this->getTitle(),
1403  $role
1404  );
1405  $allUpdates = array_merge( $allUpdates, $updates );
1406 
1407  // TODO: remove B/C hack in 1.32!
1408  $legacyUpdates = $content->getDeletionUpdates( $wikiPage );
1409 
1410  // HACK: filter out redundant and incomplete LinksDeletionUpdate
1411  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1412  return !( $update instanceof LinksDeletionUpdate );
1413  } );
1414 
1415  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1416  }
1417 
1418  // TODO: hard deprecate SecondaryDataUpdates in favor of RevisionDataUpdates in 1.33!
1419  $this->hookRunner->onRevisionDataUpdates(
1420  $this->getTitle(), $renderedRevision, $allUpdates );
1421 
1422  return $allUpdates;
1423  }
1424 
1435  public function doUpdates() {
1436  $this->assertTransition( 'done' );
1437 
1438  // TODO: move logic into a PageEventEmitter service
1439 
1440  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
1441 
1442  $legacyUser = User::newFromIdentity( $this->user );
1443 
1444  $userParserOptions = ParserOptions::newFromUser( $legacyUser );
1445  // Decide whether to save the final canonical parser ouput based on the fact that
1446  // users are typically redirected to viewing pages right after they edit those pages.
1447  // Due to vary-revision-id, getting/saving that output here might require a reparse.
1448  if ( $userParserOptions->matchesForCacheKey( $this->getCanonicalParserOptions() ) ) {
1449  // Whether getting the final output requires a reparse or not, the user will
1450  // need canonical output anyway, since that is what their parser options use.
1451  // A reparse now at least has the benefit of various warm process caches.
1452  $this->doParserCacheUpdate();
1453  } else {
1454  // If the user does not have canonical parse options, then don't risk another parse
1455  // to make output they cannot use on the page refresh that typically occurs after
1456  // editing. Doing the parser output save post-send will still benefit *other* users.
1457  DeferredUpdates::addCallableUpdate( function () {
1458  $this->doParserCacheUpdate();
1459  } );
1460  }
1461 
1462  $this->doSecondaryDataUpdates( [
1463  // T52785 do not update any other pages on a null edit
1464  'recursive' => $this->options['changed'],
1465  // Defer the getCannonicalParserOutput() call made by getSecondaryDataUpdates()
1466  'defer' => DeferredUpdates::POSTSEND
1467  ] );
1468 
1469  // TODO: MCR: check if *any* changed slot supports categories!
1470  if ( $this->rcWatchCategoryMembership
1471  && $this->getContentHandler( SlotRecord::MAIN )->supportsCategories() === true
1472  && ( $this->options['changed'] || $this->options['created'] )
1473  && !$this->options['restored']
1474  ) {
1475  // Note: jobs are pushed after deferred updates, so the job should be able to see
1476  // the recent change entry (also done via deferred updates) and carry over any
1477  // bot/deletion/IP flags, ect.
1478  $this->jobQueueGroup->lazyPush(
1480  $this->getTitle(),
1481  $this->revision->getTimestamp()
1482  )
1483  );
1484  }
1485 
1486  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1487  // @note: Extensions should *avoid* calling getCannonicalParserOutput() when using
1488  // this hook whenever possible in order to avoid unnecessary additional parses.
1489  $editInfo = $this->getPreparedEdit();
1490  $this->hookRunner->onArticleEditUpdates( $wikiPage, $editInfo, $this->options['changed'] );
1491 
1492  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1493  if ( $this->hookRunner->onArticleEditUpdatesDeleteFromRecentchanges( $wikiPage ) ) {
1494  // Flush old entries from the `recentchanges` table
1495  if ( mt_rand( 0, 9 ) == 0 ) {
1496  $this->jobQueueGroup->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
1497  }
1498  }
1499 
1500  $id = $this->getPageId();
1501  $title = $this->getTitle();
1502  $shortTitle = $title->getDBkey();
1503 
1504  if ( !$title->exists() ) {
1505  wfDebug( __METHOD__ . ": Page doesn't exist any more, bailing out" );
1506 
1507  $this->doTransition( 'done' );
1508  return;
1509  }
1510 
1511  DeferredUpdates::addCallableUpdate( function () {
1512  if (
1513  $this->options['oldcountable'] === 'no-change' ||
1514  ( !$this->options['changed'] && !$this->options['moved'] )
1515  ) {
1516  $good = 0;
1517  } elseif ( $this->options['created'] ) {
1518  $good = (int)$this->isCountable();
1519  } elseif ( $this->options['oldcountable'] !== null ) {
1520  $good = (int)$this->isCountable()
1521  - (int)$this->options['oldcountable'];
1522  } elseif ( isset( $this->pageState['oldCountable'] ) ) {
1523  $good = (int)$this->isCountable()
1524  - (int)$this->pageState['oldCountable'];
1525  } else {
1526  $good = 0;
1527  }
1528  $edits = $this->options['changed'] ? 1 : 0;
1529  $pages = $this->options['created'] ? 1 : 0;
1530 
1532  [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
1533  ) );
1534  } );
1535 
1536  // TODO: make search infrastructure aware of slots!
1537  $mainSlot = $this->revision->getSlot( SlotRecord::MAIN );
1538  if ( !$mainSlot->isInherited() && !$this->isContentDeleted() ) {
1539  DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $mainSlot->getContent() ) );
1540  }
1541 
1542  // If this is another user's talk page, update newtalk.
1543  // Don't do this if $options['changed'] = false (null-edits) nor if
1544  // it's a minor edit and the user making the edit doesn't generate notifications for those.
1545  if ( $this->options['changed']
1546  && $title->getNamespace() == NS_USER_TALK
1547  && $shortTitle != $legacyUser->getTitleKey()
1548  && !( $this->revision->isMinor() && MediaWikiServices::getInstance()
1549  ->getPermissionManager()
1550  ->userHasRight( $legacyUser, 'nominornewtalk' ) )
1551  ) {
1552  $recipient = User::newFromName( $shortTitle, false );
1553  if ( !$recipient ) {
1554  wfDebug( __METHOD__ . ": invalid username" );
1555  } else {
1556  // Allow extensions to prevent user notification
1557  // when a new message is added to their talk page
1558  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1559  if ( $this->hookRunner->onArticleEditUpdateNewTalk( $wikiPage, $recipient ) ) {
1560  $revRecord = $this->revision;
1561  $talkPageNotificationManager = MediaWikiServices::getInstance()
1562  ->getTalkPageNotificationManager();
1563  if ( User::isIP( $shortTitle ) ) {
1564  // An anonymous user
1565  $talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord );
1566  } elseif ( $recipient->isLoggedIn() ) {
1567  $talkPageNotificationManager->setUserHasNewMessages( $recipient, $revRecord );
1568  } else {
1569  wfDebug( __METHOD__ . ": don't need to notify a nonexistent user" );
1570  }
1571  }
1572  }
1573  }
1574 
1575  if ( $title->getNamespace() == NS_MEDIAWIKI
1576  && $this->getRevisionSlotsUpdate()->isModifiedSlot( SlotRecord::MAIN )
1577  ) {
1578  $mainContent = $this->isContentDeleted() ? null : $this->getRawContent( SlotRecord::MAIN );
1579 
1580  $this->messageCache->updateMessageOverride( $title, $mainContent );
1581  }
1582 
1583  // TODO: move onArticleCreate and onArticle into a PageEventEmitter service
1584  if ( $this->options['created'] ) {
1586  } elseif ( $this->options['changed'] ) { // T52785
1587  WikiPage::onArticleEdit( $title, $this->revision, $this->getTouchedSlotRoles() );
1588  } elseif ( $this->options['restored'] ) {
1589  MediaWikiServices::getInstance()->getMainWANObjectCache()->touchCheckKey(
1590  "DerivedPageDataUpdater:restore:page:$id"
1591  );
1592  }
1593 
1594  $oldRevisionRecord = $this->getParentRevision();
1595 
1596  // TODO: In the wiring, register a listener for this on the new PageEventEmitter
1598  $title,
1599  $oldRevisionRecord,
1600  $this->revision,
1601  $this->loadbalancerFactory->getLocalDomainID()
1602  );
1603 
1604  $this->doTransition( 'done' );
1605  }
1606 
1619  public function doSecondaryDataUpdates( array $options = [] ) {
1620  $this->assertHasRevision( __METHOD__ );
1621  $options += [ 'recursive' => false, 'defer' => false ];
1622  $deferValues = [ false, DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND ];
1623  if ( !in_array( $options['defer'], $deferValues, true ) ) {
1624  throw new InvalidArgumentException( 'Invalid value for defer: ' . $options['defer'] );
1625  }
1626 
1627  $triggeringUser = $this->options['triggeringUser'] ?? $this->user;
1628  if ( !$triggeringUser instanceof User ) {
1629  $triggeringUser = User::newFromIdentity( $triggeringUser );
1630  }
1631  $causeAction = $this->options['causeAction'] ?? 'unknown';
1632  $causeAgent = $this->options['causeAgent'] ?? 'unknown';
1633 
1634  // Bundle all of the data updates into a single deferred update wrapper so that
1635  // any failure will cause at most one refreshLinks job to be enqueued by
1636  // DeferredUpdates::doUpdates(). This is hard to do when there are many separate
1637  // updates that are not defined as being related.
1638  $update = new RefreshSecondaryDataUpdate(
1639  $this->loadbalancerFactory,
1640  $triggeringUser,
1641  $this->wikiPage,
1642  $this->revision,
1643  $this,
1644  [ 'recursive' => $options['recursive'] ]
1645  );
1646  $update->setCause( $causeAction, $causeAgent );
1647 
1648  if ( $options['defer'] === false ) {
1649  DeferredUpdates::attemptUpdate( $update, $this->loadbalancerFactory );
1650  } else {
1651  DeferredUpdates::addUpdate( $update, $options['defer'] );
1652  }
1653  }
1654 
1655  public function doParserCacheUpdate() {
1656  $this->assertHasRevision( __METHOD__ );
1657 
1658  $wikiPage = $this->getWikiPage(); // TODO: ParserCache should accept a RevisionRecord instead
1659 
1660  // NOTE: this may trigger the first parsing of the new content after an edit (when not
1661  // using pre-generated stashed output).
1662  // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
1663  // to be performed post-send. The client could already follow a HTTP redirect to the
1664  // page view, but would then have to wait for a response until rendering is complete.
1665  $output = $this->getCanonicalParserOutput();
1666 
1667  // Save it to the parser cache. Use the revision timestamp in the case of a
1668  // freshly saved edit, as that matches page_touched and a mismatch would trigger an
1669  // unnecessary reparse.
1670  $timestamp = $this->options['newrev'] ? $this->revision->getTimestamp()
1671  : $output->getCacheTime();
1672  $this->parserCache->save(
1673  $output, $wikiPage, $this->getCanonicalParserOptions(),
1674  $timestamp, $this->revision->getId()
1675  );
1676  }
1677 
1678 }
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:1314
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:128
MediaWiki\Storage\DerivedPageDataUpdater\$logger
LoggerInterface $logger
Definition: DerivedPageDataUpdater.php:153
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:3554
MediaWiki\Storage\DerivedPageDataUpdater\$parserCache
ParserCache $parserCache
Definition: DerivedPageDataUpdater.php:118
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:465
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:781
MediaWiki\Storage\DerivedPageDataUpdater\pageExisted
pageExisted()
Determines whether the page being edited already existed.
Definition: DerivedPageDataUpdater.php:475
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:490
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:909
MediaWiki\Storage\DerivedPageDataUpdater\$articleCountMethod
string $articleCountMethod
see $wgArticleCountMethod
Definition: DerivedPageDataUpdater.php:158
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:743
MediaWiki\Storage\DerivedPageDataUpdater\getSlotParserOutput
getSlotParserOutput( $role, $generateHtml=true)
Definition: DerivedPageDataUpdater.php:1304
MediaWiki\Storage\DerivedPageDataUpdater\$user
UserIdentity null $user
Definition: DerivedPageDataUpdater.php:108
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:152
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:1102
MediaWiki\Storage\DerivedPageDataUpdater\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: DerivedPageDataUpdater.php:246
MediaWiki\Storage\DerivedPageDataUpdater\setArticleCountMethod
setArticleCountMethod( $articleCountMethod)
Definition: DerivedPageDataUpdater.php:440
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:80
MediaWiki\Storage\DerivedPageDataUpdater\getSlots
getSlots()
Returns the slots of the target revision, after PST.
Definition: DerivedPageDataUpdater.php:1001
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:216
MediaWiki\Storage\DerivedPageDataUpdater\setRcWatchCategoryMembership
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
Definition: DerivedPageDataUpdater.php:448
MediaWiki\Storage\DerivedPageDataUpdater\getPreparedEdit
getPreparedEdit()
Definition: DerivedPageDataUpdater.php:1278
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:51
MediaWiki\Storage\DerivedPageDataUpdater\$revisionRenderer
RevisionRenderer $revisionRenderer
Definition: DerivedPageDataUpdater.php:231
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:582
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:540
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:169
MediaWiki\Storage\DerivedPageDataUpdater\$renderedRevision
RenderedRevision $renderedRevision
Definition: DerivedPageDataUpdater.php:226
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:596
MediaWiki\Storage\DerivedPageDataUpdater\$slotsUpdate
RevisionSlotsUpdate null $slotsUpdate
Definition: DerivedPageDataUpdater.php:211
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
Title\equals
equals(LinkTarget $title)
Compare with another title.
Definition: Title.php:3974
MediaWiki\Storage\DerivedPageDataUpdater\isRedirect
isRedirect()
Definition: DerivedPageDataUpdater.php:699
MediaWiki\Storage\DerivedPageDataUpdater\getCanonicalParserOptions
getCanonicalParserOptions()
Definition: DerivedPageDataUpdater.php:1321
MediaWiki\Storage\DerivedPageDataUpdater\doUpdates
doUpdates()
Do standard updates after page edit, purge, or import.
Definition: DerivedPageDataUpdater.php:1435
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
MediaWiki\Storage\DerivedPageDataUpdater\$revisionStore
RevisionStore $revisionStore
Definition: DerivedPageDataUpdater.php:123
MediaWiki\Storage\DerivedPageDataUpdater\assertHasRevision
assertHasRevision( $method)
Definition: DerivedPageDataUpdater.php:940
Revision
Definition: Revision.php:40
MediaWiki\MediaWikiServices\getInstance
static getInstance()
Returns the global default instance of the top level service locator.
Definition: MediaWikiServices.php:183
MediaWiki\Storage\DerivedPageDataUpdater\$slotRoleRegistry
SlotRoleRegistry $slotRoleRegistry
Definition: DerivedPageDataUpdater.php:234
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:377
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1026
MediaWiki\Storage\DerivedPageDataUpdater\setLogger
setLogger(LoggerInterface $logger)
Definition: DerivedPageDataUpdater.php:327
ParserOptions\newFromUserAndLang
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
Definition: ParserOptions.php:1101
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)
Definition: DerivedPageDataUpdater.php:296
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:1011
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:641
Revision\RevisionRecord\isMinor
isMinor()
MCR migration note: this replaces Revision::isMinor.
Definition: RevisionRecord.php:409
MediaWiki\Storage\DerivedPageDataUpdater\assertPrepared
assertPrepared( $method)
Definition: DerivedPageDataUpdater.php:932
User\isIP
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:950
MediaWiki\Storage\DerivedPageDataUpdater\getContentModel
getContentModel( $role)
Returns the content model of the given slot.
Definition: DerivedPageDataUpdater.php:632
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:977
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:953
MediaWiki\Storage\DerivedPageDataUpdater\assertTransition
assertTransition( $newStage)
Asserts that a transition to the given stage is possible, without performing it.
Definition: DerivedPageDataUpdater.php:360
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:909
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:962
MediaWiki\Storage\DerivedPageDataUpdater\useMaster
useMaster()
Definition: DerivedPageDataUpdater.php:646
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:592
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:45
MediaWiki\Storage\DerivedPageDataUpdater\getWikiPage
getWikiPage()
Definition: DerivedPageDataUpdater.php:463
MediaWiki\Storage\DerivedPageDataUpdater\isCountable
isCountable()
Definition: DerivedPageDataUpdater.php:654
MediaWiki\Storage\DerivedPageDataUpdater\isContentPrepared
isContentPrepared()
Whether prepareUpdate() or prepareContent() have been called on this instance.
Definition: DerivedPageDataUpdater.php:564
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:143
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:1049
MediaWiki\Storage\DerivedPageDataUpdater\$pageState
array $pageState
The state of the relevant row in page table before the edit.
Definition: DerivedPageDataUpdater.php:206
MediaWiki\Storage
Definition: BlobAccessException.php:23
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:41
MediaWiki\Storage\DerivedPageDataUpdater\$messageCache
MessageCache $messageCache
Definition: DerivedPageDataUpdater.php:138
MediaWiki\Storage\DerivedPageDataUpdater\doTransition
doTransition( $newStage)
Transition function for managing the life cycle of this instances.
Definition: DerivedPageDataUpdater.php:342
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:1040
Content
Base interface for content objects.
Definition: Content.php:35
MediaWiki\Storage\DerivedPageDataUpdater\$jobQueueGroup
JobQueueGroup $jobQueueGroup
Definition: DerivedPageDataUpdater.php:133
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:622
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:455
MediaWiki\Storage\DerivedPageDataUpdater\getSecondaryDataUpdates
getSecondaryDataUpdates( $recursive=false)
Definition: DerivedPageDataUpdater.php:1330
MediaWiki\Storage\DerivedPageDataUpdater\doParserCacheUpdate
doParserCacheUpdate()
Definition: DerivedPageDataUpdater.php:1655
Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:49
MediaWiki\Storage\DerivedPageDataUpdater\getRenderedRevision
getRenderedRevision()
Definition: DerivedPageDataUpdater.php:917
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:163
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:221
MediaWiki\Storage\DerivedPageDataUpdater\getRawSlot
getRawSlot( $role)
Returns the slot, modified or inherited, after PST, with no audience checks applied.
Definition: DerivedPageDataUpdater.php:610
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:1619
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:967
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:77
MediaWiki\Storage\DerivedPageDataUpdater\assertHasPageState
assertHasPageState( $method)
Definition: DerivedPageDataUpdater.php:923
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:571
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:148
MediaWiki\Storage\DerivedPageDataUpdater\getTouchedSlotRoles
getTouchedSlotRoles()
Returns the role names of the slots touched by the new revision, including removed roles.
Definition: DerivedPageDataUpdater.php:1030
WikiPage\onArticleEdit
static onArticleEdit(Title $title, $revRecord=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3647
MediaWiki\Storage\DerivedPageDataUpdater\$stage
string $stage
A stage identifier for managing the life cycle of this instance.
Definition: DerivedPageDataUpdater.php:244
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:612
MediaWiki\Storage\DerivedPageDataUpdater
A handle for managing updates for derived page data on edit, import, purge, etc.
Definition: DerivedPageDataUpdater.php:103
MediaWiki\Storage\DerivedPageDataUpdater\$wikiPage
WikiPage $wikiPage
Definition: DerivedPageDataUpdater.php:113
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:59
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:712
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:532
MediaWiki\Storage\DerivedPageDataUpdater\isUpdatePrepared
isUpdatePrepared()
Whether prepareUpdate() has been called on this instance.
Definition: DerivedPageDataUpdater.php:575
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:1088
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