use Content;
use ContentHandler;
use DataUpdate;
+use DeferrableUpdate;
use DeferredUpdates;
use Hooks;
use IDBAccessObject;
use InvalidArgumentException;
use JobQueueGroup;
use Language;
+use LinksDeletionUpdate;
use LinksUpdate;
use LogicException;
use MediaWiki\Edit\PreparedEdit;
+use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RenderedRevision;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionRenderer;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\User\UserIdentity;
use MessageCache;
use ParserCache;
*
* Contains the following fields:
* - oldRevision (RevisionRecord|null): the revision that was current before the change
- * associated with this update. Might not be set, use getOldRevision() instead of direct
- * access.
+ * associated with this update. Might not be set, use getParentRevision().
* - oldId (int|null): the id of the above revision. 0 if there is no such revision (the change
* was about creating a new page); null if not known (that should not happen).
* - oldIsRedirect (bool|null): whether the page was a redirect before the change. Lazy-loaded,
*/
private $slotsUpdate = null;
+ /**
+ * @var RevisionRecord|null
+ */
+ private $parentRevision = null;
+
/**
* @var RevisionRecord|null
*/
$this->jobQueueGroup = $jobQueueGroup;
$this->messageCache = $messageCache;
$this->contLang = $contLang;
- // XXX only needed for waiting for slaves to catch up; there should be a narrower
+ // XXX only needed for waiting for replicas to catch up; there should be a narrower
// interface for that.
$this->loadbalancerFactory = $loadbalancerFactory;
}
}
/**
- * Returns the revision that was current before the edit. This would be null if the edit
- * created the page, or the revision's parent for a regular edit, or the revision itself
- * for a null-edit.
- * Only defined after calling grabCurrentRevision() or prepareContent() or prepareUpdate()!
+ * Returns the parent revision of the new revision wrapped by this update.
+ * If the update is a null-edit, this will return the parent of the current (and new) revision.
+ * This will return null if the revision wrapped by this update created the page.
+ * Only defined after calling prepareContent() or prepareUpdate()!
*
- * @return RevisionRecord|null the revision that was current before the edit, or null if
- * the edit created the page.
+ * @return RevisionRecord|null the parent revision of the new revision, or null if
+ * the update created the page.
*/
- private function getOldRevision() {
- $this->assertHasPageState( __METHOD__ );
+ private function getParentRevision() {
+ $this->assertPrepared( __METHOD__ );
- // If 'oldRevision' is not set, load it!
- // Useful if $this->oldPageState is initialized by prepareUpdate.
- if ( !array_key_exists( 'oldRevision', $this->pageState ) ) {
- /** @var int $oldId */
- $oldId = $this->pageState['oldId'];
- $flags = $this->useMaster() ? RevisionStore::READ_LATEST : 0;
- $this->pageState['oldRevision'] = $oldId
- ? $this->revisionStore->getRevisionById( $oldId, $flags )
- : null;
+ if ( $this->parentRevision ) {
+ return $this->parentRevision;
}
- return $this->pageState['oldRevision'];
+ if ( !$this->pageState['oldId'] ) {
+ // If there was no current revision, there is no parent revision,
+ // since the page didn't exist.
+ return null;
+ }
+
+ $oldId = $this->revision->getParentId();
+ $flags = $this->useMaster() ? RevisionStore::READ_LATEST : 0;
+ $this->parentRevision = $oldId
+ ? $this->revisionStore->getRevisionById( $oldId, $flags )
+ : null;
+
+ return $this->parentRevision;
}
/**
* @note After prepareUpdate() was called, grabCurrentRevision() will throw an exception
* to avoid confusion, since the page's current revision is then the new revision after
* the edit, which was presumably passed to prepareUpdate() as the $revision parameter.
- * Use getOldRevision() instead to access the revision that used to be current before the
- * edit.
+ * Use getParentRevision() instead to access the revision that is the parent of the
+ * new revision.
*
* @return RevisionRecord|null the page's current revision, or null if the page does not
* yet exist.
}
// TODO: MCR: ask all slots if they have links [SlotHandler/PageTypeHandler]
- $mainContent = $this->getRawContent( 'main' );
+ $mainContent = $this->getRawContent( SlotRecord::MAIN );
return $mainContent->isCountable( $hasLinks );
}
*/
public function isRedirect() {
// NOTE: main slot determines redirect status
- $mainContent = $this->getRawContent( 'main' );
+ $mainContent = $this->getRawContent( SlotRecord::MAIN );
return $mainContent->isRedirect();
}
*/
private function revisionIsRedirect( RevisionRecord $rev ) {
// NOTE: main slot determines redirect status
- $mainContent = $rev->getContent( 'main', RevisionRecord::RAW );
+ $mainContent = $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
return $mainContent->isRedirect();
}
$stashedEdit = false;
// TODO: MCR: allow output for all slots to be stashed.
- if ( $useStash && $slotsUpdate->isModifiedSlot( 'main' ) ) {
- $mainContent = $slotsUpdate->getModifiedSlot( 'main' )->getContent();
+ if ( $useStash && $slotsUpdate->isModifiedSlot( SlotRecord::MAIN ) ) {
+ $mainContent = $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent();
$legacyUser = User::newFromIdentity( $user );
$stashedEdit = ApiStashEdit::checkCache( $title, $mainContent, $legacyUser );
}
// No PST for inherited slots! Note that "modified" slots may still be inherited
// from an earlier version, e.g. for rollbacks.
$pstSlot = $slot;
- } elseif ( $role === 'main' && $stashedEdit ) {
+ } elseif ( $role === SlotRecord::MAIN && $stashedEdit ) {
// TODO: MCR: allow PST content for all slots to be stashed.
$pstSlot = SlotRecord::newUnsaved( $role, $stashedEdit->pstContent );
} else {
// prepareUpdate() is redundant for null-edits
$this->doTransition( 'has-revision' );
+ } else {
+ $this->parentRevision = $parentRevision;
}
}
$this->assertPrepared( __METHOD__ );
if ( !$this->slotsUpdate ) {
- $old = $this->getOldRevision();
+ $old = $this->getParentRevision();
$this->slotsUpdate = RevisionSlotsUpdate::newFromRevisionSlots(
$this->revision->getSlots(),
$old ? $old->getSlots() : null
} else {
throw new LogicException(
'Trying to re-use DerivedPageDataUpdater with revision '
- .$revision->getId()
+ . $revision->getId()
. ', but it\'s already bound to revision '
. $this->revision->getId()
);
if ( !$this->user->equals( $user ) ) {
throw new LogicException(
'The Revision provided has a mismatching actor: expected '
- .$this->user->getName()
+ . $this->user->getName()
. ', got '
. $user->getName()
);
$preparedEdit->popts = $this->getCanonicalParserOptions();
$preparedEdit->output = $this->getCanonicalParserOutput();
- $preparedEdit->pstContent = $this->revision->getContent( 'main' );
+ $preparedEdit->pstContent = $this->revision->getContent( SlotRecord::MAIN );
$preparedEdit->newContent =
- $slotsUpdate->isModifiedSlot( 'main' )
- ? $slotsUpdate->getModifiedSlot( 'main' )->getContent()
- : $this->revision->getContent( 'main' ); // XXX: can we just remove this?
+ $slotsUpdate->isModifiedSlot( SlotRecord::MAIN )
+ ? $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent()
+ : $this->revision->getContent( SlotRecord::MAIN ); // XXX: can we just remove this?
$preparedEdit->oldContent = null; // unused. // XXX: could get this from the parent revision
$preparedEdit->revid = $this->revision ? $this->revision->getId() : null;
$preparedEdit->timestamp = $preparedEdit->output->getCacheTime();
/**
* @param bool $recursive
*
- * @return DataUpdate[]
+ * @return DeferrableUpdate[]
*/
public function getSecondaryDataUpdates( $recursive = false ) {
- // TODO: MCR: getSecondaryDataUpdates() needs a complete overhaul to avoid DataUpdates
- // from different slots overwriting each other in the database. Plan:
- // * replace direct calls to Content::getSecondaryDataUpdates() with calls to this method
- // * Construct LinksUpdate here, on the combined ParserOutput, instead of in AbstractContent
- // for each slot.
- // * Pass $slot into getSecondaryDataUpdates() - probably be introducing a new duplicate
- // version of this function in ContentHandler.
- // * The new method gets the PreparedEdit, but no $recursive flag (that's for LinksUpdate)
- // * Hack: call both the old and the new getSecondaryDataUpdates method here; Pass
- // the per-slot ParserOutput to the old method, for B/C.
- // * Hack: If there is more than one slot, filter LinksUpdate from the DataUpdates
- // returned by getSecondaryDataUpdates, and use a LinksUpdated for the combined output
- // instead.
- // * Call the SecondaryDataUpdates hook here (or kill it - its signature doesn't make sense)
-
- $content = $this->getSlots()->getContent( 'main' );
-
- // NOTE: $output is the combined output, to be shown in the default view.
+ if ( $this->isContentDeleted() ) {
+ // This shouldn't happen, since the current content is always public,
+ // and DataUpates are only needed for current content.
+ return [];
+ }
+
$output = $this->getCanonicalParserOutput();
- $updates = $content->getSecondaryDataUpdates(
- $this->getTitle(), null, $recursive, $output
+ // Construct a LinksUpdate for the combined canonical output.
+ $linksUpdate = new LinksUpdate(
+ $this->getTitle(),
+ $output,
+ $recursive
);
- return $updates;
+ $allUpdates = [ $linksUpdate ];
+
+ // NOTE: Run updates for all slots, not just the modified slots! Otherwise,
+ // info for an inherited slot may end up being removed. This is also needed
+ // to ensure that purges are effective.
+ $renderedRevision = $this->getRenderedRevision();
+ foreach ( $this->getSlots()->getSlotRoles() as $role ) {
+ $slot = $this->getRawSlot( $role );
+ $content = $slot->getContent();
+ $handler = $content->getContentHandler();
+
+ $updates = $handler->getSecondaryDataUpdates(
+ $this->getTitle(),
+ $content,
+ $role,
+ $renderedRevision
+ );
+ $allUpdates = array_merge( $allUpdates, $updates );
+
+ // TODO: remove B/C hack in 1.32!
+ // NOTE: we assume that the combined output contains all relevant meta-data for
+ // all slots!
+ $legacyUpdates = $content->getSecondaryDataUpdates(
+ $this->getTitle(),
+ null,
+ $recursive,
+ $output
+ );
+
+ // HACK: filter out redundant and incomplete LinksUpdates
+ $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
+ return !( $update instanceof LinksUpdate );
+ } );
+
+ $allUpdates = array_merge( $allUpdates, $legacyUpdates );
+ }
+
+ // XXX: if a slot was removed by an earlier edit, but deletion updates failed to run at
+ // that time, we don't know for which slots to run deletion updates when purging a page.
+ // We'd have to examine the entire history of the page to determine that. Perhaps there
+ // could be a "try extra hard" mode for that case that would run a DB query to find all
+ // roles/models ever used on the page. On the other hand, removing slots should be quite
+ // rare, so perhaps this isn't worth the trouble.
+
+ // TODO: consolidate with similar logic in WikiPage::getDeletionUpdates()
+ $wikiPage = $this->getWikiPage();
+ $parentRevision = $this->getParentRevision();
+ foreach ( $this->getRemovedSlotRoles() as $role ) {
+ // HACK: we should get the content model of the removed slot from a SlotRoleHandler!
+ // For now, find the slot in the parent revision - if the slot was removed, it should
+ // always exist in the parent revision.
+ $parentSlot = $parentRevision->getSlot( $role, RevisionRecord::RAW );
+ $content = $parentSlot->getContent();
+ $handler = $content->getContentHandler();
+
+ $updates = $handler->getDeletionUpdates(
+ $this->getTitle(),
+ $role
+ );
+ $allUpdates = array_merge( $allUpdates, $updates );
+
+ // TODO: remove B/C hack in 1.32!
+ $legacyUpdates = $content->getDeletionUpdates( $wikiPage );
+
+ // HACK: filter out redundant and incomplete LinksDeletionUpdate
+ $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) {
+ return !( $update instanceof LinksDeletionUpdate );
+ } );
+
+ $allUpdates = array_merge( $allUpdates, $legacyUpdates );
+ }
+
+ // TODO: hard deprecate SecondaryDataUpdates in favor of RevisionDataUpdates in 1.33!
+ Hooks::run(
+ 'RevisionDataUpdates',
+ [ $this->getTitle(), $renderedRevision, &$allUpdates ]
+ );
+
+ return $allUpdates;
}
/**
// TODO: MCR: check if *any* changed slot supports categories!
if ( $this->rcWatchCategoryMembership
- && $this->getContentHandler( 'main' )->supportsCategories() === true
+ && $this->getContentHandler( SlotRecord::MAIN )->supportsCategories() === true
&& ( $this->options['changed'] || $this->options['created'] )
&& !$this->options['restored']
) {
// the recent change entry (also done via deferred updates) and carry over any
// bot/deletion/IP flags, ect.
$this->jobQueueGroup->lazyPush(
- new CategoryMembershipChangeJob(
+ CategoryMembershipChangeJob::newSpec(
$this->getTitle(),
- [
- 'pageId' => $this->getPageId(),
- 'revTimestamp' => $this->revision->getTimestamp(),
- ]
+ $this->revision->getTimestamp()
)
);
}
) );
// TODO: make search infrastructure aware of slots!
- $mainSlot = $this->revision->getSlot( 'main' );
+ $mainSlot = $this->revision->getSlot( SlotRecord::MAIN );
if ( !$mainSlot->isInherited() && !$this->isContentDeleted() ) {
DeferredUpdates::addUpdate( new SearchUpdate( $id, $dbKey, $mainSlot->getContent() ) );
}
}
if ( $title->getNamespace() == NS_MEDIAWIKI
- && $this->getRevisionSlotsUpdate()->isModifiedSlot( 'main' )
+ && $this->getRevisionSlotsUpdate()->isModifiedSlot( SlotRecord::MAIN )
) {
- $mainContent = $this->isContentDeleted() ? null : $this->getRawContent( 'main' );
+ $mainContent = $this->isContentDeleted() ? null : $this->getRawContent( SlotRecord::MAIN );
$this->messageCache->updateMessageOverride( $title, $mainContent );
}
WikiPage::onArticleEdit( $title, $legacyRevision, $this->getTouchedSlotRoles() );
}
- $oldRevision = $this->getOldRevision();
+ $oldRevision = $this->getParentRevision();
$oldLegacyRevision = $oldRevision ? new Revision( $oldRevision ) : null;
// TODO: In the wiring, register a listener for this on the new PageEventEmitter
}
foreach ( $updates as $update ) {
- $update->setCause( $causeAction, $causeAgent );
+ if ( $update instanceof DataUpdate ) {
+ $update->setCause( $causeAction, $causeAgent );
+ }
if ( $update instanceof LinksUpdate ) {
$update->setRevision( $legacyRevision );
$update->setTriggeringUser( $triggeringUser );