33use InvalidArgumentException;
55use Psr\Log\LoggerAwareInterface;
56use Psr\Log\LoggerInterface;
57use Psr\Log\NullLogger;
65use Wikimedia\Assert\Assert;
169 'oldrevision' =>
null,
170 'oldcountable' =>
null,
171 'oldredirect' =>
null,
172 'triggeringUser' =>
null,
175 'causeAction' =>
null,
176 'causeAgent' =>
null,
251 'knows-current' =>
true,
252 'has-content' =>
true,
253 'has-revision' =>
true,
256 'knows-current' =>
true,
257 'has-content' =>
true,
258 'has-revision' =>
true,
261 'has-content' =>
true,
262 'has-revision' =>
true,
265 'has-revision' =>
true,
304 $this->logger =
new NullLogger();
326 $this->stage = $newStage;
341 if ( empty( self::$transitions[$this->stage][$newStage] ) ) {
342 throw new LogicException(
"Cannot transition from {$this->stage} to $newStage" );
367 throw new InvalidArgumentException(
'$parentId should match the parent of $revision' );
383 if ( $this->pageState
391 if ( $this->pageState
392 && $parentId !==
null
393 && $this->pageState[
'oldId'] !== $parentId
399 if ( $this->slotsUpdate
437 return $this->wikiPage->getTitle();
458 return $this->pageState[
'oldId'] > 0;
473 if ( $this->parentRevision ) {
477 if ( !$this->pageState[
'oldId'] ) {
484 $flags = $this->
useMaster() ? RevisionStore::READ_LATEST : 0;
485 $this->parentRevision = $oldId
486 ? $this->revisionStore->getRevisionById( $oldId, $flags )
513 if ( $this->pageState ) {
514 return $this->pageState[
'oldRevision'];
526 $current = $rev ? $rev->getRevisionRecord() :
null;
529 'oldRevision' => $current,
530 'oldId' => $rev ? $rev->getId() : 0,
537 return $this->pageState[
'oldRevision'];
546 return $this->revision !==
null;
557 return $this->revision !==
null && $this->revision->getId() !==
null;
565 return $this->wikiPage->getId();
574 if ( $this->revision ) {
575 return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
592 return $this->
getSlots()->getSlot( $role );
604 return $this->
getRawSlot( $role )->getContent();
614 return $this->
getRawSlot( $role )->getModel();
623 return ContentHandler::getForModelID( $this->
getContentModel( $role ) );
628 return $this->wikiPage->wasLoadedFrom( self::READ_LATEST );
637 if ( !$this->
getTitle()->isContentPage() ) {
653 if ( $this->articleCountMethod ===
'link' ) {
662 foreach ( $this->
getSlots()->getSlotRoles() as $role ) {
663 $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
664 if ( $roleHandler->supportsArticleCount() ) {
667 if (
$content->isCountable( $hasLinks ) ) {
684 return $mainContent->isRedirect();
694 $mainContent = $rev->
getContent( SlotRecord::MAIN, RevisionRecord::RAW );
696 return $mainContent->isRedirect();
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.'
737 throw new LogicException(
'Can\'t call prepareContent() again for different user! '
738 .
'Expected ' . $this->user->getName() .
', got ' .
$user->
getName()
742 if ( !$this->slotsUpdate->hasSameUpdates(
$slotsUpdate ) ) {
743 throw new LogicException(
744 'Can\'t call prepareContent() again with different slot content!'
759 $stashedEdit =
false;
764 $stashedEdit = $editStash->checkCache(
771 $userPopts = ParserOptions::newFromUserAndLang(
$user, $this->contLang );
772 Hooks::run(
'ArticlePrepareTextForEdit', [
$wikiPage, $userPopts ] );
778 $this->revision = MutableRevisionRecord::newFromParentRevision(
$parentRevision );
786 $this->revision->setUser(
$user );
789 $oldCallback = $userPopts->getCurrentRevisionCallback();
790 $userPopts->setCurrentRevisionCallback(
791 function (
Title $parserTitle, $parser =
false ) use (
$title, $oldCallback ) {
793 $legacyRevision = new Revision( $this->revision );
794 return $legacyRevision;
796 return call_user_func( $oldCallback, $parserTitle, $parser );
801 $pstContentSlots = $this->revision->getSlots();
806 if ( $slot->isInherited() ) {
810 } elseif ( $role === SlotRecord::MAIN && $stashedEdit ) {
812 $pstSlot = SlotRecord::newUnsaved( $role, $stashedEdit->pstContent );
815 $pstContent =
$content->preSaveTransform(
$title, $this->user, $userPopts );
816 $pstSlot = SlotRecord::newUnsaved( $role, $pstContent );
819 $pstContentSlots->setSlot( $pstSlot );
823 $pstContentSlots->removeSlot( $role );
832 if ( !$this->options[
'changed'] ) {
852 $renderHints = [
'use-master' => $this->
useMaster(),
'audience' => RevisionRecord::RAW ];
854 if ( $stashedEdit ) {
856 $output = $stashedEdit->output;
858 $output->setCacheTime( $stashedEdit->timestamp );
860 $renderHints[
'known-revision-output'] = $output;
862 $this->logger->debug( __METHOD__ .
': using stashed edit output...' );
867 $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
905 if ( !$this->pageState ) {
906 throw new LogicException(
907 'Must call grabCurrentRevision() or prepareContent() '
908 .
'or prepareUpdate() before calling ' . $method
914 if ( !$this->revision ) {
915 throw new LogicException(
916 'Must call prepareContent() or prepareUpdate() before calling ' . $method
922 if ( !$this->revision->getId() ) {
923 throw new LogicException(
924 'Must call prepareUpdate() before calling ' . $method
936 return $this->options[
'created'];
950 return $this->options[
'changed'];
961 if ( $this->pageState[
'oldIsRedirect'] ===
null ) {
963 $rev = $this->pageState[
'oldRevision'];
967 $this->pageState[
'oldIsRedirect'] =
false;
971 return $this->pageState[
'oldIsRedirect'];
984 return $this->revision->getSlots();
995 if ( !$this->slotsUpdate ) {
998 $this->revision->getSlots(),
999 $old ? $old->getSlots() : null
1087 '$options["oldrevision"]',
1088 'must be a RevisionRecord (or Revision)'
1091 !isset(
$options[
'triggeringUser'] )
1093 '$options["triggeringUser"]',
1094 'must be a UserIdentity'
1098 throw new InvalidArgumentException(
1099 'Revision must have an ID set for it to be used with prepareUpdate()!'
1103 if ( $this->revision && $this->revision->getId() ) {
1107 throw new LogicException(
1108 'Trying to re-use DerivedPageDataUpdater with revision '
1110 .
', but it\'s already bound to revision '
1111 . $this->revision->getId()
1116 if ( $this->revision
1119 throw new LogicException(
1120 'The Revision provided has mismatching content!'
1127 if ( $this->revision ) {
1128 $oldId = $this->pageState[
'oldId'] ?? 0;
1130 } elseif ( isset( $this->options[
'oldrevision'] ) ) {
1132 $oldRev = $this->options[
'oldrevision'];
1133 $oldId = $oldRev->getId();
1139 if ( $oldId !==
null ) {
1148 $this->options[
'changed'] =
true;
1151 $this->options[
'changed'] =
false;
1154 throw new LogicException(
1155 'The Revision mismatches old revision ID: '
1156 .
'Old ID is ' . $oldId
1167 if ( $this->user !==
null && $this->options[
'changed'] && $this->slotsUpdate ) {
1169 if ( !$this->user->equals(
$user ) ) {
1170 throw new LogicException(
1171 'The Revision provided has a mismatching actor: expected '
1172 . $this->user->getName()
1181 if ( !$this->pageState ) {
1182 $this->pageState = [
1183 'oldIsRedirect' => isset( $this->options[
'oldredirect'] )
1184 && is_bool( $this->options[
'oldredirect'] )
1185 ? $this->options[
'oldredirect']
1187 'oldCountable' => isset( $this->options[
'oldcountable'] )
1188 && is_bool( $this->options[
'oldcountable'] )
1189 ? $this->options[
'oldcountable']
1193 if ( $this->options[
'changed'] ) {
1197 if ( isset( $this->options[
'oldrevision'] ) ) {
1198 $rev = $this->options[
'oldrevision'];
1199 $this->pageState[
'oldRevision'] = $rev instanceof
Revision
1206 $this->pageState[
'oldRevision'] =
$revision;
1211 $this->options[
'created'] = ( $this->options[
'created'] ||
1212 ( $this->pageState[
'oldId'] === 0 ) );
1220 if ( !$this->user ) {
1225 if ( $this->renderedRevision ) {
1226 $this->renderedRevision->updateRevision(
$revision );
1230 $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
1236 'audience' => RevisionRecord::RAW,
1237 'known-revision-output' =>
$options[
'known-revision-output'] ??
null
1260 $preparedEdit->parserOutputCallback = [ $this,
'getCanonicalParserOutput' ];
1261 $preparedEdit->pstContent = $this->revision->getContent( SlotRecord::MAIN );
1262 $preparedEdit->newContent =
1265 : $this->revision->getContent( SlotRecord::MAIN );
1266 $preparedEdit->oldContent =
null;
1267 $preparedEdit->revid = $this->revision ? $this->revision->getId() :
null;
1268 $preparedEdit->timestamp = $preparedEdit->output->getCacheTime();
1269 $preparedEdit->format = $preparedEdit->pstContent->getDefaultFormat();
1271 return $preparedEdit;
1282 [
'generate-html' => $generateHtml ]
1321 $allUpdates = [ $linksUpdate ];
1327 foreach ( $this->
getSlots()->getSlotRoles() as $role ) {
1330 $handler =
$content->getContentHandler();
1332 $updates = $handler->getSecondaryDataUpdates(
1338 $allUpdates = array_merge( $allUpdates, $updates );
1343 $legacyUpdates =
$content->getSecondaryDataUpdates(
1351 $legacyUpdates = array_filter( $legacyUpdates,
function ( $update ) {
1355 $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1373 $content = $parentSlot->getContent();
1374 $handler =
$content->getContentHandler();
1376 $updates = $handler->getDeletionUpdates(
1380 $allUpdates = array_merge( $allUpdates, $updates );
1386 $legacyUpdates = array_filter( $legacyUpdates,
function ( $update ) {
1390 $allUpdates = array_merge( $allUpdates, $legacyUpdates );
1395 'RevisionDataUpdates',
1420 $legacyRevision =
new Revision( $this->revision );
1422 $userParserOptions = ParserOptions::newFromUser( $legacyUser );
1426 if ( $userParserOptions->matchesForCacheKey( $this->getCanonicalParserOptions() ) ) {
1435 DeferredUpdates::addCallableUpdate(
function () {
1446 'recursive' => $this->options[
'changed']
1452 DeferredUpdates::addUpdate( $wrapperUpdate );
1455 if ( $this->rcWatchCategoryMembership
1457 && ( $this->options[
'changed'] || $this->options[
'created'] )
1458 && !$this->options[
'restored']
1463 $this->jobQueueGroup->lazyPush(
1466 $this->revision->getTimestamp()
1475 Hooks::run(
'ArticleEditUpdates',
1476 [ &
$wikiPage, &$editInfo, $this->options[
'changed'] ] );
1479 if ( Hooks::run(
'ArticleEditUpdatesDeleteFromRecentchanges', [ &
$wikiPage ] ) ) {
1481 if ( mt_rand( 0, 9 ) == 0 ) {
1488 $shortTitle =
$title->getDBkey();
1490 if ( !
$title->exists() ) {
1491 wfDebug( __METHOD__ .
": Page doesn't exist any more, bailing out\n" );
1497 DeferredUpdates::addCallableUpdate(
function () {
1499 $this->options[
'oldcountable'] ===
'no-change' ||
1500 ( !$this->options[
'changed'] && !$this->options[
'moved'] )
1503 } elseif ( $this->options[
'created'] ) {
1505 } elseif ( $this->options[
'oldcountable'] !==
null ) {
1507 - (int)$this->options[
'oldcountable'];
1508 } elseif ( isset( $this->pageState[
'oldCountable'] ) ) {
1510 - (int)$this->pageState[
'oldCountable'];
1514 $edits = $this->options[
'changed'] ? 1 : 0;
1515 $pages = $this->options[
'created'] ? 1 : 0;
1518 [
'edits' => $edits,
'articles' => $good,
'pages' => $pages ]
1523 $mainSlot = $this->revision->getSlot( SlotRecord::MAIN );
1525 DeferredUpdates::addUpdate(
new SearchUpdate( $id,
$title, $mainSlot->getContent() ) );
1531 if ( $this->options[
'changed']
1533 && $shortTitle != $legacyUser->getTitleKey()
1535 ->getPermissionManager()
1536 ->userHasRight( $legacyUser,
'nominornewtalk' ) )
1539 if ( !$recipient ) {
1540 wfDebug( __METHOD__ .
": invalid username\n" );
1545 if ( Hooks::run(
'ArticleEditUpdateNewTalk', [ &
$wikiPage, $recipient ] ) ) {
1548 $recipient->setNewtalk(
true, $legacyRevision );
1549 } elseif ( $recipient->isLoggedIn() ) {
1550 $recipient->setNewtalk(
true, $legacyRevision );
1552 wfDebug( __METHOD__ .
": don't need to notify a nonexistent user\n" );
1559 && $this->getRevisionSlotsUpdate()->isModifiedSlot( SlotRecord::MAIN )
1563 $this->messageCache->updateMessageOverride(
$title, $mainContent );
1567 if ( $this->options[
'created'] ) {
1568 WikiPage::onArticleCreate(
$title );
1569 } elseif ( $this->options[
'changed'] ) {
1574 $oldLegacyRevision = $oldRevision ?
new Revision( $oldRevision ) :
null;
1581 $this->loadbalancerFactory->getLocalDomainID()
1601 $options += [
'recursive' =>
false,
'defer' => false ];
1602 $deferValues = [
false, DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND ];
1603 if ( !in_array(
$options[
'defer'], $deferValues,
true ) ) {
1604 throw new InvalidArgumentException(
'Invalid value for defer: ' .
$options[
'defer'] );
1608 $triggeringUser = $this->options[
'triggeringUser'] ??
$this->user;
1609 if ( !$triggeringUser instanceof
User ) {
1612 $causeAction = $this->options[
'causeAction'] ??
'unknown';
1613 $causeAgent = $this->options[
'causeAgent'] ??
'unknown';
1614 $legacyRevision =
new Revision( $this->revision );
1616 foreach ( $updates as $update ) {
1618 $update->setCause( $causeAction, $causeAgent );
1621 $update->setRevision( $legacyRevision );
1622 $update->setTriggeringUser( $triggeringUser );
1626 if (
$options[
'defer'] ===
false ) {
1628 $this->loadbalancerFactory->commitMasterChanges( __METHOD__ );
1629 foreach ( $updates as $update ) {
1630 DeferredUpdates::attemptUpdate( $update, $this->loadbalancerFactory );
1633 foreach ( $updates as $update ) {
1634 DeferredUpdates::addUpdate( $update,
$options[
'defer'] );
1654 $timestamp = $this->options[
'newrev'] ? $this->revision->getTimestamp()
1655 : $output->getCacheTime();
1656 $this->parserCache->save(
1658 $timestamp, $this->revision->
getId()
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Job to add recent change entries mentioning category membership changes.
static newSpec(Title $title, $revisionTimestamp)
A content handler knows how do deal with a specific type of content on a wiki page.
Abstract base class for update jobs that do something with some secondary data extracted from article...
Class for managing the deferred updates.
Class to handle enqueueing of background jobs.
Internationalisation code.
Update object handling the cleanup of links tables after a page was deleted.
Class the manages updates of *_link tables as well as similar extension-managed tables.
Deferrable Update for closure/callback.
setTransactionRoundRequirement( $mode)
A handle for managing updates for derived page data on edit, import, purge, etc.
getSecondaryDataUpdates( $recursive=false)
MessageCache $messageCache
getModifiedSlotRoles()
Returns the role names of the slots modified by the new revision, not including removed roles.
setRcWatchCategoryMembership( $rcWatchCategoryMembership)
static array[] $transitions
Transition table for managing the life cycle of DerivedPageDateUpdater instances.
string $stage
A stage identifier for managing the life cycle of this instance.
getParentRevision()
Returns the parent revision of the new revision wrapped by this update.
revisionIsRedirect(RevisionRecord $rev)
array $pageState
The state of the relevant row in page table before the edit.
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
isContentDeleted()
Whether the content is deleted and thus not visible to the public.
getContentHandler( $role)
prepareUpdate(RevisionRecord $revision, array $options=[])
Prepare derived data updates targeting the given Revision.
RevisionSlotsUpdate null $slotsUpdate
string $articleCountMethod
see $wgArticleCountMethod
RevisionStore $revisionStore
doTransition( $newStage)
Transition function for managing the life cycle of this instances.
isCreation()
Whether the edit creates the page.
__construct(WikiPage $wikiPage, RevisionStore $revisionStore, RevisionRenderer $revisionRenderer, SlotRoleRegistry $slotRoleRegistry, ParserCache $parserCache, JobQueueGroup $jobQueueGroup, MessageCache $messageCache, Language $contLang, ILBFactory $loadbalancerFactory)
ILBFactory $loadbalancerFactory
getRemovedSlotRoles()
Returns the role names of the slots removed by the new revision.
getCanonicalParserOptions()
setLogger(LoggerInterface $logger)
assertHasPageState( $method)
getRawContent( $role)
Returns the content of the given slot, with no audience checks.
getSlotParserOutput( $role, $generateHtml=true)
getRevisionSlotsUpdate()
Returns the RevisionSlotsUpdate for this updater.
RevisionRecord null $parentRevision
getCanonicalParserOutput()
RenderedRevision $renderedRevision
isChange()
Whether the edit created, or should create, a new revision (that is, it's not a null-edit).
grabCurrentRevision()
Returns the revision that was the page's current revision when grabCurrentRevision() was first called...
doUpdates()
Do standard updates after page edit, purge, or import.
pageExisted()
Determines whether the page being edited already existed.
wasRedirect()
Whether the page was a redirect before the edit.
isUpdatePrepared()
Whether prepareUpdate() has been called on this instance.
getContentModel( $role)
Returns the content model of the given slot.
boolean $rcWatchCategoryMembership
see $wgRCWatchCategoryMembership
getSlots()
Returns the slots of the target revision, after PST.
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...
getTouchedSlotRoles()
Returns the role names of the slots touched by the new revision, including removed roles.
RevisionRecord null $revision
RevisionRenderer $revisionRenderer
isContentPrepared()
Whether prepareUpdate() or prepareContent() have been called on this instance.
assertHasRevision( $method)
getRawSlot( $role)
Returns the slot, modified or inherited, after PST, with no audience checks applied.
$options
Stores (most of) the $options parameter of prepareUpdate().
JobQueueGroup $jobQueueGroup
setArticleCountMethod( $articleCountMethod)
assertTransition( $newStage)
Asserts that a transition to the given stage is possible, without performing it.
getRevision()
Returns the update's target revision - that is, the revision that will be the current revision after ...
prepareContent(User $user, RevisionSlotsUpdate $slotsUpdate, $useStash=true)
Prepare updates based on an update which has not yet been saved.
SlotRoleRegistry $slotRoleRegistry
Cache of messages that are defined by MediaWiki namespace pages or by hooks.
Set options of the Parser.
Job for pruning recent changes.
Abstraction for ResourceLoader modules which pull from wiki pages.
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...
Database independant search index updater.
Class for handling updates to the site_stats table.
static factory(array $deltas)
Represents a title within MediaWiki.
equals(LinkTarget $title)
Compare with another title.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
static isIP( $name)
Does the string match an anonymous IP address?
Class representing a MediaWiki article and history.
getRevision()
Get the latest revision.
isRedirect()
Tests if the article content represents a redirect.
loadPageData( $from='fromdb')
Load the object from a given source by title.
isCountable( $editInfo=false)
Determine whether a page would be suitable for being counted as an article in the site_stats table ba...
Base interface for content objects.
Interface that deferrable updates should implement.
Interface for database access objects.