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