MediaWiki  master
DerivedPageDataUpdater.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Storage;
24 
26 use Content;
31 use Hooks;
60 use Title;
61 use User;
65 
98 
102  private $user = null;
103 
107  private $wikiPage;
108 
112  private $parserCache;
113 
117  private $revisionStore;
118 
122  private $contLang;
123 
127  private $jobQueueGroup;
128 
132  private $messageCache;
133 
138 
143 
147  private $rcWatchCategoryMembership = false;
148 
153  private $options = [
154  'changed' => true,
155  // newrev is true if prepareUpdate is handling the creation of a new revision,
156  // as opposed to a null edit or a forced update.
157  'newrev' => false,
158  'created' => false,
159  'moved' => false,
160  'restored' => false,
161  'oldrevision' => null,
162  'oldcountable' => null,
163  'oldredirect' => null,
164  'triggeringUser' => null,
165  // causeAction/causeAgent default to 'unknown' but that's handled where it's read,
166  // to make the life of prepareUpdate() callers easier.
167  'causeAction' => null,
168  'causeAgent' => null,
169  ];
170 
190  private $pageState = null;
191 
195  private $slotsUpdate = null;
196 
201 
205  private $revision = null;
206 
211 
216 
219 
228  private $stage = 'new';
229 
240  private static $transitions = [
241  'new' => [
242  'new' => true,
243  'knows-current' => true,
244  'has-content' => true,
245  'has-revision' => true,
246  ],
247  'knows-current' => [
248  'knows-current' => true,
249  'has-content' => true,
250  'has-revision' => true,
251  ],
252  'has-content' => [
253  'has-content' => true,
254  'has-revision' => true,
255  ],
256  'has-revision' => [
257  'has-revision' => true,
258  'done' => true,
259  ],
260  ];
261 
273  public function __construct(
283  ) {
284  $this->wikiPage = $wikiPage;
285 
286  $this->parserCache = $parserCache;
287  $this->revisionStore = $revisionStore;
288  $this->revisionRenderer = $revisionRenderer;
289  $this->slotRoleRegistry = $slotRoleRegistry;
290  $this->jobQueueGroup = $jobQueueGroup;
291  $this->messageCache = $messageCache;
292  $this->contLang = $contLang;
293  // XXX only needed for waiting for replicas to catch up; there should be a narrower
294  // interface for that.
295  $this->loadbalancerFactory = $loadbalancerFactory;
296  }
297 
309  private function doTransition( $newStage ) {
310  $this->assertTransition( $newStage );
311 
312  $oldStage = $this->stage;
313  $this->stage = $newStage;
314 
315  return $oldStage;
316  }
317 
327  private function assertTransition( $newStage ) {
328  if ( empty( self::$transitions[$this->stage][$newStage] ) ) {
329  throw new LogicException( "Cannot transition from {$this->stage} to $newStage" );
330  }
331  }
332 
336  private function getWikiId() {
337  // TODO: get from RevisionStore
338  return false;
339  }
340 
352  public function isReusableFor(
356  $parentId = null
357  ) {
358  if ( $revision
359  && $parentId
360  && $revision->getParentId() !== $parentId
361  ) {
362  throw new InvalidArgumentException( '$parentId should match the parent of $revision' );
363  }
364 
365  // NOTE: For null revisions, $user may be different from $this->revision->getUser
366  // and also from $revision->getUser.
367  // But $user should always match $this->user.
368  if ( $user && $this->user && $user->getName() !== $this->user->getName() ) {
369  return false;
370  }
371 
372  if ( $revision && $this->revision && $this->revision->getId()
373  && $this->revision->getId() !== $revision->getId()
374  ) {
375  return false;
376  }
377 
378  if ( $this->pageState
379  && $revision
380  && $revision->getParentId() !== null
381  && $this->pageState['oldId'] !== $revision->getParentId()
382  ) {
383  return false;
384  }
385 
386  if ( $this->pageState
387  && $parentId !== null
388  && $this->pageState['oldId'] !== $parentId
389  ) {
390  return false;
391  }
392 
393  // NOTE: this check is the primary reason for having the $this->slotsUpdate field!
394  if ( $this->slotsUpdate
395  && $slotsUpdate
396  && !$this->slotsUpdate->hasSameUpdates( $slotsUpdate )
397  ) {
398  return false;
399  }
400 
401  if ( $revision
402  && $this->revision
403  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
404  ) {
405  return false;
406  }
407 
408  return true;
409  }
410 
416  $this->articleCountMethod = $articleCountMethod;
417  }
418 
424  $this->rcWatchCategoryMembership = $rcWatchCategoryMembership;
425  }
426 
430  private function getTitle() {
431  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
432  return $this->wikiPage->getTitle();
433  }
434 
438  private function getWikiPage() {
439  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
440  return $this->wikiPage;
441  }
442 
450  public function pageExisted() {
451  $this->assertHasPageState( __METHOD__ );
452 
453  return $this->pageState['oldId'] > 0;
454  }
455 
465  private function getParentRevision() {
466  $this->assertPrepared( __METHOD__ );
467 
468  if ( $this->parentRevision ) {
469  return $this->parentRevision;
470  }
471 
472  if ( !$this->pageState['oldId'] ) {
473  // If there was no current revision, there is no parent revision,
474  // since the page didn't exist.
475  return null;
476  }
477 
478  $oldId = $this->revision->getParentId();
479  $flags = $this->useMaster() ? RevisionStore::READ_LATEST : 0;
480  $this->parentRevision = $oldId
481  ? $this->revisionStore->getRevisionById( $oldId, $flags )
482  : null;
483 
484  return $this->parentRevision;
485  }
486 
507  public function grabCurrentRevision() {
508  if ( $this->pageState ) {
509  return $this->pageState['oldRevision'];
510  }
511 
512  $this->assertTransition( 'knows-current' );
513 
514  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
515  $wikiPage = $this->getWikiPage();
516 
517  // Do not call WikiPage::clear(), since the caller may already have caused page data
518  // to be loaded with SELECT FOR UPDATE. Just assert it's loaded now.
519  $wikiPage->loadPageData( self::READ_LATEST );
521  $current = $rev ? $rev->getRevisionRecord() : null;
522 
523  $this->pageState = [
524  'oldRevision' => $current,
525  'oldId' => $rev ? $rev->getId() : 0,
526  'oldIsRedirect' => $wikiPage->isRedirect(), // NOTE: uses page table
527  'oldCountable' => $wikiPage->isCountable(), // NOTE: uses pagelinks table
528  ];
529 
530  $this->doTransition( 'knows-current' );
531 
532  return $this->pageState['oldRevision'];
533  }
534 
540  public function isContentPrepared() {
541  return $this->revision !== null;
542  }
543 
551  public function isUpdatePrepared() {
552  return $this->revision !== null && $this->revision->getId() !== null;
553  }
554 
558  private function getPageId() {
559  // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
560  return $this->wikiPage->getId();
561  }
562 
568  public function isContentDeleted() {
569  if ( $this->revision ) {
570  // XXX: if that revision is the current revision, this should be skipped
571  return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
572  } else {
573  // If the content has not been saved yet, it cannot have been deleted yet.
574  return false;
575  }
576  }
577 
587  public function getRawSlot( $role ) {
588  return $this->getSlots()->getSlot( $role );
589  }
590 
599  public function getRawContent( $role ) {
600  return $this->getRawSlot( $role )->getContent();
601  }
602 
609  private function getContentModel( $role ) {
610  return $this->getRawSlot( $role )->getModel();
611  }
612 
617  private function getContentHandler( $role ) {
618  // TODO: inject something like a ContentHandlerRegistry
619  return ContentHandler::getForModelID( $this->getContentModel( $role ) );
620  }
621 
622  private function useMaster() {
623  // TODO: can we just set a flag to true in prepareContent()?
624  return $this->wikiPage->wasLoadedFrom( self::READ_LATEST );
625  }
626 
630  public function isCountable() {
631  // NOTE: Keep in sync with WikiPage::isCountable.
632 
633  if ( !$this->getTitle()->isContentPage() ) {
634  return false;
635  }
636 
637  if ( $this->isContentDeleted() ) {
638  // This should be irrelevant: countability only applies to the current revision,
639  // and the current revision is never suppressed.
640  return false;
641  }
642 
643  if ( $this->isRedirect() ) {
644  return false;
645  }
646 
647  $hasLinks = null;
648 
649  if ( $this->articleCountMethod === 'link' ) {
650  // NOTE: it would be more appropriate to determine for each slot separately
651  // whether it has links, and use that information with that slot's
652  // isCountable() method. However, that would break parity with
653  // WikiPage::isCountable, which uses the pagelinks table to determine
654  // whether the current revision has links.
655  $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() );
656  }
657 
658  foreach ( $this->getModifiedSlotRoles() as $role ) {
659  $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
660  if ( $roleHandler->supportsArticleCount() ) {
661  $content = $this->getRawContent( $role );
662 
663  if ( $content->isCountable( $hasLinks ) ) {
664  return true;
665  }
666  }
667  }
668 
669  return false;
670  }
671 
675  public function isRedirect() {
676  // NOTE: main slot determines redirect status
677  // TODO: MCR: this should be controlled by a PageTypeHandler
678  $mainContent = $this->getRawContent( SlotRecord::MAIN );
679 
680  return $mainContent->isRedirect();
681  }
682 
688  private function revisionIsRedirect( RevisionRecord $rev ) {
689  // NOTE: main slot determines redirect status
690  $mainContent = $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
691 
692  return $mainContent->isRedirect();
693  }
694 
719  public function prepareContent(
720  User $user,
722  $useStash = true
723  ) {
724  if ( $this->slotsUpdate ) {
725  if ( !$this->user ) {
726  throw new LogicException(
727  'Unexpected state: $this->slotsUpdate was initialized, '
728  . 'but $this->user was not.'
729  );
730  }
731 
732  if ( $this->user->getName() !== $user->getName() ) {
733  throw new LogicException( 'Can\'t call prepareContent() again for different user! '
734  . 'Expected ' . $this->user->getName() . ', got ' . $user->getName()
735  );
736  }
737 
738  if ( !$this->slotsUpdate->hasSameUpdates( $slotsUpdate ) ) {
739  throw new LogicException(
740  'Can\'t call prepareContent() again with different slot content!'
741  );
742  }
743 
744  return; // prepareContent() already done, nothing to do
745  }
746 
747  $this->assertTransition( 'has-content' );
748 
749  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
750  $title = $this->getTitle();
751 
753 
754  // The edit may have already been prepared via api.php?action=stashedit
755  $stashedEdit = false;
756 
757  // TODO: MCR: allow output for all slots to be stashed.
758  if ( $useStash && $slotsUpdate->isModifiedSlot( SlotRecord::MAIN ) ) {
759  $editStash = MediaWikiServices::getInstance()->getPageEditStash();
760  $stashedEdit = $editStash->checkCache(
761  $title,
762  $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent(),
763  User::newFromIdentity( $user )
764  );
765  }
766 
767  $userPopts = ParserOptions::newFromUserAndLang( $user, $this->contLang );
768  Hooks::run( 'ArticlePrepareTextForEdit', [ $wikiPage, $userPopts ] );
769 
770  $this->user = $user;
771  $this->slotsUpdate = $slotsUpdate;
772 
773  if ( $parentRevision ) {
775  } else {
776  $this->revision = new MutableRevisionRecord( $title );
777  }
778 
779  // NOTE: user and timestamp must be set, so they can be used for
780  // {{subst:REVISIONUSER}} and {{subst:REVISIONTIMESTAMP}} in PST!
781  $this->revision->setTimestamp( wfTimestampNow() );
782  $this->revision->setUser( $user );
783 
784  // Set up ParserOptions to operate on the new revision
785  $oldCallback = $userPopts->getCurrentRevisionCallback();
786  $userPopts->setCurrentRevisionCallback(
787  function ( Title $parserTitle, $parser = false ) use ( $title, $oldCallback ) {
788  if ( $parserTitle->equals( $title ) ) {
789  $legacyRevision = new Revision( $this->revision );
790  return $legacyRevision;
791  } else {
792  return call_user_func( $oldCallback, $parserTitle, $parser );
793  }
794  }
795  );
796 
797  $pstContentSlots = $this->revision->getSlots();
798 
799  foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
800  $slot = $slotsUpdate->getModifiedSlot( $role );
801 
802  if ( $slot->isInherited() ) {
803  // No PST for inherited slots! Note that "modified" slots may still be inherited
804  // from an earlier version, e.g. for rollbacks.
805  $pstSlot = $slot;
806  } elseif ( $role === SlotRecord::MAIN && $stashedEdit ) {
807  // TODO: MCR: allow PST content for all slots to be stashed.
808  $pstSlot = SlotRecord::newUnsaved( $role, $stashedEdit->pstContent );
809  } else {
810  $content = $slot->getContent();
811  $pstContent = $content->preSaveTransform( $title, $this->user, $userPopts );
812  $pstSlot = SlotRecord::newUnsaved( $role, $pstContent );
813  }
814 
815  $pstContentSlots->setSlot( $pstSlot );
816  }
817 
818  foreach ( $slotsUpdate->getRemovedRoles() as $role ) {
819  $pstContentSlots->removeSlot( $role );
820  }
821 
822  $this->options['created'] = ( $parentRevision === null );
823  $this->options['changed'] = ( $parentRevision === null
824  || !$pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
825 
826  $this->doTransition( 'has-content' );
827 
828  if ( !$this->options['changed'] ) {
829  // null-edit!
830 
831  // TODO: move this into MutableRevisionRecord
832  // TODO: This needs to behave differently for a forced dummy edit!
833  $this->revision->setId( $parentRevision->getId() );
834  $this->revision->setTimestamp( $parentRevision->getTimestamp() );
835  $this->revision->setPageId( $parentRevision->getPageId() );
836  $this->revision->setParentId( $parentRevision->getParentId() );
837  $this->revision->setUser( $parentRevision->getUser( RevisionRecord::RAW ) );
838  $this->revision->setComment( $parentRevision->getComment( RevisionRecord::RAW ) );
839  $this->revision->setMinorEdit( $parentRevision->isMinor() );
840  $this->revision->setVisibility( $parentRevision->getVisibility() );
841 
842  // prepareUpdate() is redundant for null-edits
843  $this->doTransition( 'has-revision' );
844  } else {
845  $this->parentRevision = $parentRevision;
846  }
847 
848  $renderHints = [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ];
849 
850  if ( $stashedEdit ) {
852  $output = $stashedEdit->output;
853 
854  // TODO: this should happen when stashing the ParserOutput, not now!
855  $output->setCacheTime( $stashedEdit->timestamp );
856 
857  $renderHints['known-revision-output'] = $output;
858  }
859 
860  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
861  // NOTE: the revision is either new or current, so we can bypass audience checks.
862  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
863  $this->revision,
864  null,
865  null,
866  $renderHints
867  );
868  }
869 
885  public function getRevision() {
886  $this->assertPrepared( __METHOD__ );
887  return $this->revision;
888  }
889 
893  public function getRenderedRevision() {
894  $this->assertPrepared( __METHOD__ );
895 
897  }
898 
899  private function assertHasPageState( $method ) {
900  if ( !$this->pageState ) {
901  throw new LogicException(
902  'Must call grabCurrentRevision() or prepareContent() '
903  . 'or prepareUpdate() before calling ' . $method
904  );
905  }
906  }
907 
908  private function assertPrepared( $method ) {
909  if ( !$this->revision ) {
910  throw new LogicException(
911  'Must call prepareContent() or prepareUpdate() before calling ' . $method
912  );
913  }
914  }
915 
916  private function assertHasRevision( $method ) {
917  if ( !$this->revision->getId() ) {
918  throw new LogicException(
919  'Must call prepareUpdate() before calling ' . $method
920  );
921  }
922  }
923 
929  public function isCreation() {
930  $this->assertPrepared( __METHOD__ );
931  return $this->options['created'];
932  }
933 
943  public function isChange() {
944  $this->assertPrepared( __METHOD__ );
945  return $this->options['changed'];
946  }
947 
953  public function wasRedirect() {
954  $this->assertHasPageState( __METHOD__ );
955 
956  if ( $this->pageState['oldIsRedirect'] === null ) {
958  $rev = $this->pageState['oldRevision'];
959  if ( $rev ) {
960  $this->pageState['oldIsRedirect'] = $this->revisionIsRedirect( $rev );
961  } else {
962  $this->pageState['oldIsRedirect'] = false;
963  }
964  }
965 
966  return $this->pageState['oldIsRedirect'];
967  }
968 
977  public function getSlots() {
978  $this->assertPrepared( __METHOD__ );
979  return $this->revision->getSlots();
980  }
981 
987  private function getRevisionSlotsUpdate() {
988  $this->assertPrepared( __METHOD__ );
989 
990  if ( !$this->slotsUpdate ) {
991  $old = $this->getParentRevision();
992  $this->slotsUpdate = RevisionSlotsUpdate::newFromRevisionSlots(
993  $this->revision->getSlots(),
994  $old ? $old->getSlots() : null
995  );
996  }
997  return $this->slotsUpdate;
998  }
999 
1006  public function getTouchedSlotRoles() {
1007  return $this->getRevisionSlotsUpdate()->getTouchedRoles();
1008  }
1009 
1016  public function getModifiedSlotRoles() {
1017  return $this->getRevisionSlotsUpdate()->getModifiedRoles();
1018  }
1019 
1025  public function getRemovedSlotRoles() {
1026  return $this->getRevisionSlotsUpdate()->getRemovedRoles();
1027  }
1028 
1072  public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
1073  Assert::parameter(
1074  !isset( $options['oldrevision'] )
1075  || $options['oldrevision'] instanceof Revision
1076  || $options['oldrevision'] instanceof RevisionRecord,
1077  '$options["oldrevision"]',
1078  'must be a RevisionRecord (or Revision)'
1079  );
1080  Assert::parameter(
1081  !isset( $options['triggeringUser'] )
1082  || $options['triggeringUser'] instanceof UserIdentity,
1083  '$options["triggeringUser"]',
1084  'must be a UserIdentity'
1085  );
1086 
1087  if ( !$revision->getId() ) {
1088  throw new InvalidArgumentException(
1089  'Revision must have an ID set for it to be used with prepareUpdate()!'
1090  );
1091  }
1092 
1093  if ( $this->revision && $this->revision->getId() ) {
1094  if ( $this->revision->getId() === $revision->getId() ) {
1095  return; // nothing to do!
1096  } else {
1097  throw new LogicException(
1098  'Trying to re-use DerivedPageDataUpdater with revision '
1099  . $revision->getId()
1100  . ', but it\'s already bound to revision '
1101  . $this->revision->getId()
1102  );
1103  }
1104  }
1105 
1106  if ( $this->revision
1107  && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
1108  ) {
1109  throw new LogicException(
1110  'The Revision provided has mismatching content!'
1111  );
1112  }
1113 
1114  // Override fields defined in $this->options with values from $options.
1115  $this->options = array_intersect_key( $options, $this->options ) + $this->options;
1116 
1117  if ( $this->revision ) {
1118  $oldId = $this->pageState['oldId'] ?? 0;
1119  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1120  } elseif ( isset( $this->options['oldrevision'] ) ) {
1122  $oldRev = $this->options['oldrevision'];
1123  $oldId = $oldRev->getId();
1124  $this->options['newrev'] = ( $revision->getId() !== $oldId );
1125  } else {
1126  $oldId = $revision->getParentId();
1127  }
1128 
1129  if ( $oldId !== null ) {
1130  // XXX: what if $options['changed'] disagrees?
1131  // MovePage creates a dummy revision with changed = false!
1132  // We may want to explicitly distinguish between "no new revision" (null-edit)
1133  // and "new revision without new content" (dummy revision).
1134 
1135  if ( $oldId === $revision->getParentId() ) {
1136  // NOTE: this may still be a NullRevision!
1137  // New revision!
1138  $this->options['changed'] = true;
1139  } elseif ( $oldId === $revision->getId() ) {
1140  // Null-edit!
1141  $this->options['changed'] = false;
1142  } else {
1143  // This indicates that calling code has given us the wrong Revision object
1144  throw new LogicException(
1145  'The Revision mismatches old revision ID: '
1146  . 'Old ID is ' . $oldId
1147  . ', parent ID is ' . $revision->getParentId()
1148  . ', revision ID is ' . $revision->getId()
1149  );
1150  }
1151  }
1152 
1153  // If prepareContent() was used to generate the PST content (which is indicated by
1154  // $this->slotsUpdate being set), and this is not a null-edit, then the given
1155  // revision must have the acting user as the revision author. Otherwise, user
1156  // signatures generated by PST would mismatch the user in the revision record.
1157  if ( $this->user !== null && $this->options['changed'] && $this->slotsUpdate ) {
1158  $user = $revision->getUser();
1159  if ( !$this->user->equals( $user ) ) {
1160  throw new LogicException(
1161  'The Revision provided has a mismatching actor: expected '
1162  . $this->user->getName()
1163  . ', got '
1164  . $user->getName()
1165  );
1166  }
1167  }
1168 
1169  // If $this->pageState was not yet initialized by grabCurrentRevision or prepareContent,
1170  // emulate the state of the page table before the edit, as good as we can.
1171  if ( !$this->pageState ) {
1172  $this->pageState = [
1173  'oldIsRedirect' => isset( $this->options['oldredirect'] )
1174  && is_bool( $this->options['oldredirect'] )
1175  ? $this->options['oldredirect']
1176  : null,
1177  'oldCountable' => isset( $this->options['oldcountable'] )
1178  && is_bool( $this->options['oldcountable'] )
1179  ? $this->options['oldcountable']
1180  : null,
1181  ];
1182 
1183  if ( $this->options['changed'] ) {
1184  // The edit created a new revision
1185  $this->pageState['oldId'] = $revision->getParentId();
1186 
1187  if ( isset( $this->options['oldrevision'] ) ) {
1188  $rev = $this->options['oldrevision'];
1189  $this->pageState['oldRevision'] = $rev instanceof Revision
1190  ? $rev->getRevisionRecord()
1191  : $rev;
1192  }
1193  } else {
1194  // This is a null-edit, so the old revision IS the new revision!
1195  $this->pageState['oldId'] = $revision->getId();
1196  $this->pageState['oldRevision'] = $revision;
1197  }
1198  }
1199 
1200  // "created" is forced here
1201  $this->options['created'] = ( $this->pageState['oldId'] === 0 );
1202 
1203  $this->revision = $revision;
1204 
1205  $this->doTransition( 'has-revision' );
1206 
1207  // NOTE: in case we have a User object, don't override with a UserIdentity.
1208  // We already checked that $revision->getUser() mathces $this->user;
1209  if ( !$this->user ) {
1210  $this->user = $revision->getUser( RevisionRecord::RAW );
1211  }
1212 
1213  // Prune any output that depends on the revision ID.
1214  if ( $this->renderedRevision ) {
1215  $this->renderedRevision->updateRevision( $revision );
1216  } else {
1217 
1218  // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
1219  // NOTE: the revision is either new or current, so we can bypass audience checks.
1220  $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
1221  $this->revision,
1222  null,
1223  null,
1224  [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ]
1225  );
1226 
1227  // XXX: Since we presumably are dealing with the current revision,
1228  // we could try to get the ParserOutput from the parser cache.
1229  }
1230 
1231  // TODO: optionally get ParserOutput from the ParserCache here.
1232  // Move the logic used by RefreshLinksJob here!
1233  }
1234 
1239  public function getPreparedEdit() {
1240  $this->assertPrepared( __METHOD__ );
1241 
1243  $preparedEdit = new PreparedEdit();
1244 
1245  $preparedEdit->popts = $this->getCanonicalParserOptions();
1246  $preparedEdit->parserOutputCallback = [ $this, 'getCanonicalParserOutput' ];
1247  $preparedEdit->pstContent = $this->revision->getContent( SlotRecord::MAIN );
1248  $preparedEdit->newContent =
1250  ? $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent()
1251  : $this->revision->getContent( SlotRecord::MAIN ); // XXX: can we just remove this?
1252  $preparedEdit->oldContent = null; // unused. // XXX: could get this from the parent revision
1253  $preparedEdit->revid = $this->revision ? $this->revision->getId() : null;
1254  $preparedEdit->timestamp = $preparedEdit->output->getCacheTime();
1255  $preparedEdit->format = $preparedEdit->pstContent->getDefaultFormat();
1256 
1257  return $preparedEdit;
1258  }
1259 
1265  public function getSlotParserOutput( $role, $generateHtml = true ) {
1266  return $this->getRenderedRevision()->getSlotParserOutput(
1267  $role,
1268  [ 'generate-html' => $generateHtml ]
1269  );
1270  }
1271 
1275  public function getCanonicalParserOutput() {
1276  return $this->getRenderedRevision()->getRevisionParserOutput();
1277  }
1278 
1282  public function getCanonicalParserOptions() {
1283  return $this->getRenderedRevision()->getOptions();
1284  }
1285 
1291  public function getSecondaryDataUpdates( $recursive = false ) {
1292  if ( $this->isContentDeleted() ) {
1293  // This shouldn't happen, since the current content is always public,
1294  // and DataUpates are only needed for current content.
1295  return [];
1296  }
1297 
1298  $output = $this->getCanonicalParserOutput();
1299 
1300  // Construct a LinksUpdate for the combined canonical output.
1301  $linksUpdate = new LinksUpdate(
1302  $this->getTitle(),
1303  $output,
1304  $recursive
1305  );
1306 
1307  $allUpdates = [ $linksUpdate ];
1308 
1309  // NOTE: Run updates for all slots, not just the modified slots! Otherwise,
1310  // info for an inherited slot may end up being removed. This is also needed
1311  // to ensure that purges are effective.
1313  foreach ( $this->getSlots()->getSlotRoles() as $role ) {
1314  $slot = $this->getRawSlot( $role );
1315  $content = $slot->getContent();
1316  $handler = $content->getContentHandler();
1317 
1318  $updates = $handler->getSecondaryDataUpdates(
1319  $this->getTitle(),
1320  $content,
1321  $role,
1323  );
1324  $allUpdates = array_merge( $allUpdates, $updates );
1325 
1326  // TODO: remove B/C hack in 1.32!
1327  // NOTE: we assume that the combined output contains all relevant meta-data for
1328  // all slots!
1329  $legacyUpdates = $content->getSecondaryDataUpdates(
1330  $this->getTitle(),
1331  null,
1332  $recursive,
1333  $output
1334  );
1335 
1336  // HACK: filter out redundant and incomplete LinksUpdates
1337  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1338  return !( $update instanceof LinksUpdate );
1339  } );
1340 
1341  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1342  }
1343 
1344  // XXX: if a slot was removed by an earlier edit, but deletion updates failed to run at
1345  // that time, we don't know for which slots to run deletion updates when purging a page.
1346  // We'd have to examine the entire history of the page to determine that. Perhaps there
1347  // could be a "try extra hard" mode for that case that would run a DB query to find all
1348  // roles/models ever used on the page. On the other hand, removing slots should be quite
1349  // rare, so perhaps this isn't worth the trouble.
1350 
1351  // TODO: consolidate with similar logic in WikiPage::getDeletionUpdates()
1352  $wikiPage = $this->getWikiPage();
1354  foreach ( $this->getRemovedSlotRoles() as $role ) {
1355  // HACK: we should get the content model of the removed slot from a SlotRoleHandler!
1356  // For now, find the slot in the parent revision - if the slot was removed, it should
1357  // always exist in the parent revision.
1358  $parentSlot = $parentRevision->getSlot( $role, RevisionRecord::RAW );
1359  $content = $parentSlot->getContent();
1360  $handler = $content->getContentHandler();
1361 
1362  $updates = $handler->getDeletionUpdates(
1363  $this->getTitle(),
1364  $role
1365  );
1366  $allUpdates = array_merge( $allUpdates, $updates );
1367 
1368  // TODO: remove B/C hack in 1.32!
1369  $legacyUpdates = $content->getDeletionUpdates( $wikiPage );
1370 
1371  // HACK: filter out redundant and incomplete LinksDeletionUpdate
1372  $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
1373  return !( $update instanceof LinksDeletionUpdate );
1374  } );
1375 
1376  $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1377  }
1378 
1379  // TODO: hard deprecate SecondaryDataUpdates in favor of RevisionDataUpdates in 1.33!
1380  Hooks::run(
1381  'RevisionDataUpdates',
1382  [ $this->getTitle(), $renderedRevision, &$allUpdates ]
1383  );
1384 
1385  return $allUpdates;
1386  }
1387 
1398  public function doUpdates() {
1399  $this->assertTransition( 'done' );
1400 
1401  // TODO: move logic into a PageEventEmitter service
1402 
1403  $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
1404 
1405  $legacyUser = User::newFromIdentity( $this->user );
1406  $legacyRevision = new Revision( $this->revision );
1407 
1408  $userParserOptions = ParserOptions::newFromUser( $legacyUser );
1409  // Decide whether to save the final canonical parser ouput based on the fact that
1410  // users are typically redirected to viewing pages right after they edit those pages.
1411  // Due to vary-revision-id, getting/saving that output here might require a reparse.
1412  if ( $userParserOptions->matchesForCacheKey( $this->getCanonicalParserOptions() ) ) {
1413  // Whether getting the final output requires a reparse or not, the user will
1414  // need canonical output anyway, since that is what their parser options use.
1415  // A reparse now at least has the benefit of various warm process caches.
1416  $this->doParserCacheUpdate();
1417  } else {
1418  // If the user does not have canonical parse options, then don't risk another parse
1419  // to make output they cannot use on the page refresh that typically occurs after
1420  // editing. Doing the parser output save post-send will still benefit *other* users.
1421  DeferredUpdates::addCallableUpdate( function () {
1422  $this->doParserCacheUpdate();
1423  } );
1424  }
1425 
1426  // Defer the getCannonicalParserOutput() call triggered by getSecondaryDataUpdates()
1427  // by wrapping the code that schedules the secondary updates in a callback itself
1428  $wrapperUpdate = new MWCallableUpdate(
1429  function () {
1430  $this->doSecondaryDataUpdates( [
1431  // T52785 do not update any other pages on a null edit
1432  'recursive' => $this->options['changed']
1433  ] );
1434  },
1435  __METHOD__
1436  );
1437  $wrapperUpdate->setTransactionRoundRequirement( $wrapperUpdate::TRX_ROUND_ABSENT );
1438  DeferredUpdates::addUpdate( $wrapperUpdate );
1439 
1440  // TODO: MCR: check if *any* changed slot supports categories!
1441  if ( $this->rcWatchCategoryMembership
1442  && $this->getContentHandler( SlotRecord::MAIN )->supportsCategories() === true
1443  && ( $this->options['changed'] || $this->options['created'] )
1444  && !$this->options['restored']
1445  ) {
1446  // Note: jobs are pushed after deferred updates, so the job should be able to see
1447  // the recent change entry (also done via deferred updates) and carry over any
1448  // bot/deletion/IP flags, ect.
1449  $this->jobQueueGroup->lazyPush(
1451  $this->getTitle(),
1452  $this->revision->getTimestamp()
1453  )
1454  );
1455  }
1456 
1457  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1458  // @note: Extensions should *avoid* calling getCannonicalParserOutput() when using
1459  // this hook whenever possible in order to avoid unnecessary additional parses.
1460  $editInfo = $this->getPreparedEdit();
1461  Hooks::run( 'ArticleEditUpdates',
1462  [ &$wikiPage, &$editInfo, $this->options['changed'] ] );
1463 
1464  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1465  if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
1466  // Flush old entries from the `recentchanges` table
1467  if ( mt_rand( 0, 9 ) == 0 ) {
1468  $this->jobQueueGroup->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
1469  }
1470  }
1471 
1472  $id = $this->getPageId();
1473  $title = $this->getTitle();
1474  $dbKey = $title->getPrefixedDBkey();
1475  $shortTitle = $title->getDBkey();
1476 
1477  if ( !$title->exists() ) {
1478  wfDebug( __METHOD__ . ": Page doesn't exist any more, bailing out\n" );
1479 
1480  $this->doTransition( 'done' );
1481  return;
1482  }
1483 
1484  DeferredUpdates::addCallableUpdate( function () {
1485  if (
1486  $this->options['oldcountable'] === 'no-change' ||
1487  ( !$this->options['changed'] && !$this->options['moved'] )
1488  ) {
1489  $good = 0;
1490  } elseif ( $this->options['created'] ) {
1491  $good = (int)$this->isCountable();
1492  } elseif ( $this->options['oldcountable'] !== null ) {
1493  $good = (int)$this->isCountable()
1494  - (int)$this->options['oldcountable'];
1495  } elseif ( isset( $this->pageState['oldCountable'] ) ) {
1496  $good = (int)$this->isCountable()
1497  - (int)$this->pageState['oldCountable'];
1498  } else {
1499  $good = 0;
1500  }
1501  $edits = $this->options['changed'] ? 1 : 0;
1502  $pages = $this->options['created'] ? 1 : 0;
1503 
1505  [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
1506  ) );
1507  } );
1508 
1509  // TODO: make search infrastructure aware of slots!
1510  $mainSlot = $this->revision->getSlot( SlotRecord::MAIN );
1511  if ( !$mainSlot->isInherited() && !$this->isContentDeleted() ) {
1512  DeferredUpdates::addUpdate( new SearchUpdate( $id, $dbKey, $mainSlot->getContent() ) );
1513  }
1514 
1515  // If this is another user's talk page, update newtalk.
1516  // Don't do this if $options['changed'] = false (null-edits) nor if
1517  // it's a minor edit and the user making the edit doesn't generate notifications for those.
1518  if ( $this->options['changed']
1519  && $title->getNamespace() == NS_USER_TALK
1520  && $shortTitle != $legacyUser->getTitleKey()
1521  && !( $this->revision->isMinor() && $legacyUser->isAllowed( 'nominornewtalk' ) )
1522  ) {
1523  $recipient = User::newFromName( $shortTitle, false );
1524  if ( !$recipient ) {
1525  wfDebug( __METHOD__ . ": invalid username\n" );
1526  } else {
1527  // Allow extensions to prevent user notification
1528  // when a new message is added to their talk page
1529  // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
1530  if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
1531  if ( User::isIP( $shortTitle ) ) {
1532  // An anonymous user
1533  $recipient->setNewtalk( true, $legacyRevision );
1534  } elseif ( $recipient->isLoggedIn() ) {
1535  $recipient->setNewtalk( true, $legacyRevision );
1536  } else {
1537  wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
1538  }
1539  }
1540  }
1541  }
1542 
1543  if ( $title->getNamespace() == NS_MEDIAWIKI
1544  && $this->getRevisionSlotsUpdate()->isModifiedSlot( SlotRecord::MAIN )
1545  ) {
1546  $mainContent = $this->isContentDeleted() ? null : $this->getRawContent( SlotRecord::MAIN );
1547 
1548  $this->messageCache->updateMessageOverride( $title, $mainContent );
1549  }
1550 
1551  // TODO: move onArticleCreate and onArticle into a PageEventEmitter service
1552  if ( $this->options['created'] ) {
1554  } elseif ( $this->options['changed'] ) { // T52785
1555  WikiPage::onArticleEdit( $title, $legacyRevision, $this->getTouchedSlotRoles() );
1556  }
1557 
1558  $oldRevision = $this->getParentRevision();
1559  $oldLegacyRevision = $oldRevision ? new Revision( $oldRevision ) : null;
1560 
1561  // TODO: In the wiring, register a listener for this on the new PageEventEmitter
1563  $title, $oldLegacyRevision, $legacyRevision, $this->getWikiId() ?: wfWikiID()
1564  );
1565 
1566  $this->doTransition( 'done' );
1567  }
1568 
1581  public function doSecondaryDataUpdates( array $options = [] ) {
1582  $this->assertHasRevision( __METHOD__ );
1583  $options += [ 'recursive' => false, 'defer' => false ];
1584  $deferValues = [ false, DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND ];
1585  if ( !in_array( $options['defer'], $deferValues, true ) ) {
1586  throw new InvalidArgumentException( 'Invalid value for defer: ' . $options['defer'] );
1587  }
1588  $updates = $this->getSecondaryDataUpdates( $options['recursive'] );
1589 
1590  $triggeringUser = $this->options['triggeringUser'] ?? $this->user;
1591  if ( !$triggeringUser instanceof User ) {
1592  $triggeringUser = User::newFromIdentity( $triggeringUser );
1593  }
1594  $causeAction = $this->options['causeAction'] ?? 'unknown';
1595  $causeAgent = $this->options['causeAgent'] ?? 'unknown';
1596  $legacyRevision = new Revision( $this->revision );
1597 
1598  foreach ( $updates as $update ) {
1599  if ( $update instanceof DataUpdate ) {
1600  $update->setCause( $causeAction, $causeAgent );
1601  }
1602  if ( $update instanceof LinksUpdate ) {
1603  $update->setRevision( $legacyRevision );
1604  $update->setTriggeringUser( $triggeringUser );
1605  }
1606  }
1607 
1608  if ( $options['defer'] === false ) {
1609  // T221577: flush any transaction; each update needs outer transaction scope
1610  $this->loadbalancerFactory->commitMasterChanges( __METHOD__ );
1611  foreach ( $updates as $update ) {
1612  DeferredUpdates::attemptUpdate( $update, $this->loadbalancerFactory );
1613  }
1614  } else {
1615  foreach ( $updates as $update ) {
1616  DeferredUpdates::addUpdate( $update, $options['defer'] );
1617  }
1618  }
1619  }
1620 
1621  public function doParserCacheUpdate() {
1622  $this->assertHasRevision( __METHOD__ );
1623 
1624  $wikiPage = $this->getWikiPage(); // TODO: ParserCache should accept a RevisionRecord instead
1625 
1626  // NOTE: this may trigger the first parsing of the new content after an edit (when not
1627  // using pre-generated stashed output).
1628  // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
1629  // to be performed post-send. The client could already follow a HTTP redirect to the
1630  // page view, but would then have to wait for a response until rendering is complete.
1631  $output = $this->getCanonicalParserOutput();
1632 
1633  // Save it to the parser cache. Use the revision timestamp in the case of a
1634  // freshly saved edit, as that matches page_touched and a mismatch would trigger an
1635  // unnecessary reparse.
1636  $timestamp = $this->options['newrev'] ? $this->revision->getTimestamp()
1637  : $output->getCacheTime();
1638  $this->parserCache->save(
1640  $timestamp, $this->revision->getId()
1641  );
1642  }
1643 
1644 }
In both all secondary updates will be triggered handle like object that caches derived data representing a and can trigger updates of cached copies of that e g in the links the ParserCache
Definition: pageupdater.txt:78
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:3375
doUpdates()
Do standard updates after page edit, purge, or import.
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...
isMinor()
MCR migration note: this replaces Revision::isMinor.
getRemovedSlotRoles()
Returns the role names of the slots removed by the new revision.
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:485
static invalidateModuleCache(Title $title, Revision $old=null, Revision $new=null, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:985
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
equals(LinkTarget $title)
Compare with another title.
Definition: Title.php:3985
Represents information returned by WikiPage::prepareContentForEdit()
The RevisionRenderer service provides access to rendered output for revisions.
getContentModel( $role)
Returns the content model of the given slot.
getSlots()
Returns the slots defined for this revision.
static attemptUpdate(DeferrableUpdate $update, ILBFactory $lbFactory)
Attempt to run an update with the appropriate transaction round state it expects. ...
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
getRevisionSlotsUpdate()
Returns the RevisionSlotsUpdate for this updater.
static array [] __construct(WikiPage $wikiPage, RevisionStore $revisionStore, RevisionRenderer $revisionRenderer, SlotRoleRegistry $slotRoleRegistry, ParserCache $parserCache, JobQueueGroup $jobQueueGroup, MessageCache $messageCache, Language $contLang, ILBFactory $loadbalancerFactory)
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page...
isUpdatePrepared()
Whether prepareUpdate() has been called on this instance.
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
Value object representing a modification of revision slots.
getTouchedSlotRoles()
Returns the role names of the slots touched by the new revision, including removed roles...
wasRedirect()
Whether the page was a redirect before the edit.
static getInstance()
Returns the global default instance of the top level service locator.
isContentDeleted()
Whether the content is deleted and thus not visible to the public.
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition: hooks.txt:1799
either a unescaped string or a HtmlArmor object after in associative array form externallinks $linksUpdate
Definition: hooks.txt:2061
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2311
pageExisted()
Determines whether the page being edited already existed.
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:630
isContentPrepared()
Whether prepareUpdate() or prepareContent() have been called on this instance.
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place $output
Definition: hooks.txt:2217
Mutable RevisionRecord implementation, for building new revision entries programmatically.
getRevision()
Returns the update&#39;s target revision - that is, the revision that will be the current revision after ...
string $articleCountMethod
see $wgArticleCountMethod
static onArticleEdit(Title $title, Revision $revision=null, $slotsChanged=null)
Purge caches on page update etc.
Definition: WikiPage.php:3462
Interface for objects representing user identity.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:780
prepareContent(User $user, RevisionSlotsUpdate $slotsUpdate, $useStash=true)
Prepare updates based on an update which has not yet been saved.
getRevision()
Get the latest revision.
Definition: WikiPage.php:783
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
getRawContent( $role)
Returns the content of the given slot, with no audience checks.
static newFromRevisionSlots(RevisionSlots $newSlots, RevisionSlots $parentSlots=null)
Constructs a RevisionSlotsUpdate representing the update that turned $parentSlots into $newSlots...
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
getVisibility()
Get the deletion bitfield of the revision.
static factory(array $deltas)
array $pageState
The state of the relevant row in page table before the edit.
boolean $rcWatchCategoryMembership
see $wgRCWatchCategoryMembership
Service for looking up page revisions.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same user
Wikitext formatted, in the key only.
Definition: distributors.txt:9
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
isChange()
Whether the edit created, or should create, a new revision (that is, it&#39;s not a null-edit).
hasSameUpdates(RevisionSlotsUpdate $other)
Returns true if $other represents the same update - that is, if all methods defined by RevisionSlotsU...
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
$options
Stores (most of) the $options parameter of prepareUpdate().
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1766
isModifiedSlot( $role)
Returns whether getModifiedSlot() will return a SlotRecord for the given role.
getRawSlot( $role)
Returns the slot, modified or inherited, after PST, with no audience checks applied.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:661
static newFromParentRevision(RevisionRecord $parent)
Returns an incomplete MutableRevisionRecord which uses $parent as its parent revision, and inherits all slots form it.
grabCurrentRevision()
Returns the revision that was the page&#39;s current revision when grabCurrentRevision() was first called...
const NS_MEDIAWIKI
Definition: Defines.php:68
string $stage
A stage identifier for managing the life cycle of this instance.
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:942
getSlot( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns meta-data for the given slot.
getId()
Get revision ID.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
A handle for managing updates for derived page data on edit, import, purge, etc.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing options(say) and put it in one place. Instead of having little title-reversing if-blocks spread all over the codebase in showAnArticle
An interface for generating database load balancers.
Definition: ILBFactory.php:33
getModifiedRoles()
Returns a list of modified slot roles, that is, roles modified by calling modifySlot(), and not later removed by calling removeSlot().
static newSpec(Title $title, $revisionTimestamp)
getParentRevision()
Returns the parent revision of the new revision wrapped by this update.
static newFromUser( $user)
Get a ParserOptions object from a given user.
getModifiedSlot( $role)
Returns the SlotRecord associated with the given role, if the slot with that role was modified (and n...
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
getSlots()
Returns the slots of the target revision, after PST.
In both all secondary updates will be triggered handle like object that caches derived data representing a revision
Definition: pageupdater.txt:78
isCreation()
Whether the edit creates the page.
static newUnsaved( $role, Content $content)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:129
Page revision base class.
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision&#39;s author&#39;s user identity, if it&#39;s available to the specified audience.
prepareUpdate(RevisionRecord $revision, array $options=[])
Prepare derived data updates targeting the given Revision.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:594
getRemovedRoles()
Returns a list of removed slot roles, that is, roles removed by calling removeSlot(), and not later re-introduced by calling modifySlot().
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it&#39;s available to the specified audience.
assertTransition( $newStage)
Asserts that a transition to the given stage is possible, without performing it.
$content
Definition: pageupdater.txt:72
const NS_USER_TALK
Definition: Defines.php:63
getParentId()
Get parent revision ID (the original previous page revision).
doTransition( $newStage)
Transition function for managing the life cycle of this instances.
static array [] $transitions
Transition table for managing the life cycle of DerivedPageDateUpdater instances. ...
getPageId()
Get the page ID.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
getModifiedSlotRoles()
Returns the role names of the slots modified by the new revision, not including removed roles...