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