use LinksUpdate;
use LogicException;
use MediaWiki\Edit\PreparedEdit;
-use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RenderedRevision;
+use MediaWiki\Revision\RevisionRenderer;
use MediaWiki\User\UserIdentity;
use MessageCache;
use ParserCache;
use ParserOptions;
use ParserOutput;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
use RecentChangesUpdateJob;
use ResourceLoaderWikiModule;
use Revision;
use Title;
use User;
use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\LBFactory;
use WikiPage;
/**
*/
private $contLang;
- /**
- * @var LoggerInterface
- */
- private $saveParseLogger;
-
/**
* @var JobQueueGroup
*/
*/
private $messageCache;
+ /**
+ * @var LBFactory
+ */
+ private $loadbalancerFactory;
+
/**
* @var string see $wgArticleCountMethod
*/
private $rcWatchCategoryMembership = false;
/**
- * See $options on prepareUpdate.
+ * Stores (most of) the $options parameter of prepareUpdate().
+ * @see prepareUpdate()
*/
private $options = [
'changed' => true,
'created' => false,
'moved' => false,
'restored' => false,
+ 'oldrevision' => null,
'oldcountable' => null,
'oldredirect' => null,
+ 'triggeringUser' => null,
+ // causeAction/causeAgent default to 'unknown' but that's handled where it's read,
+ // to make the life of prepareUpdate() callers easier.
+ 'causeAction' => null,
+ 'causeAgent' => null,
];
/**
private $slotsUpdate = null;
/**
- * @var MutableRevisionSlots|null
- */
- private $pstContentSlots = null;
-
- /**
- * @var object[] anonymous objects with two fields, using slot roles as keys:
- * - hasHtml: whether the output contains HTML
- * - ParserOutput: the slot's parser output
- */
- private $slotsOutput = [];
-
- /**
- * @var ParserOutput|null
+ * @var RevisionRecord|null
*/
- private $canonicalParserOutput = null;
+ private $revision = null;
/**
- * @var ParserOptions|null
+ * @var RenderedRevision
*/
- private $canonicalParserOptions = null;
+ private $renderedRevision = null;
/**
- * @var RevisionRecord
+ * @var RevisionRenderer
*/
- private $revision = null;
+ private $revisionRenderer;
/**
* A stage identifier for managing the life cycle of this instance.
/**
* @param WikiPage $wikiPage ,
* @param RevisionStore $revisionStore
+ * @param RevisionRenderer $revisionRenderer
* @param ParserCache $parserCache
* @param JobQueueGroup $jobQueueGroup
* @param MessageCache $messageCache
* @param Language $contLang
- * @param LoggerInterface|null $saveParseLogger
+ * @param LBFactory $loadbalancerFactory
*/
public function __construct(
WikiPage $wikiPage,
RevisionStore $revisionStore,
+ RevisionRenderer $revisionRenderer,
ParserCache $parserCache,
JobQueueGroup $jobQueueGroup,
MessageCache $messageCache,
Language $contLang,
- LoggerInterface $saveParseLogger = null
+ LBFactory $loadbalancerFactory
) {
$this->wikiPage = $wikiPage;
$this->parserCache = $parserCache;
$this->revisionStore = $revisionStore;
+ $this->revisionRenderer = $revisionRenderer;
$this->jobQueueGroup = $jobQueueGroup;
$this->messageCache = $messageCache;
$this->contLang = $contLang;
-
- // XXX: replace all wfDebug calls with a Logger. Do we nede more than one logger here?
- $this->saveParseLogger = $saveParseLogger ?: new NullLogger();
+ // XXX only needed for waiting for slaves to catch up; there should be a narrower
+ // interface for that.
+ $this->loadbalancerFactory = $loadbalancerFactory;
}
/**
return false;
}
- if ( $revision && $this->revision && $this->revision->getId() !== $revision->getId() ) {
+ if ( $revision && $this->revision && $this->revision->getId()
+ && $this->revision->getId() !== $revision->getId()
+ ) {
return false;
}
if ( $this->revision
&& $user
+ && $this->revision->getUser( RevisionRecord::RAW )
&& $this->revision->getUser( RevisionRecord::RAW )->getName() !== $user->getName()
) {
return false;
if ( $revision
&& $this->user
+ && $this->revision->getUser( RevisionRecord::RAW )
&& $revision->getUser( RevisionRecord::RAW )->getName() !== $this->user->getName()
) {
return false;
return false;
}
- if ( $this->pstContentSlots
- && $revision
- && !$this->pstContentSlots->hasSameContent( $revision->getSlots() )
+ if ( $revision
+ && $this->revision
+ && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
) {
return false;
}
* @return bool
*/
public function isContentPrepared() {
- return $this->pstContentSlots !== null;
+ return $this->revision !== null;
}
/**
* Whether prepareUpdate() has been called on this instance.
*
+ * @note will also return null in case of a null-edit!
+ *
* @return bool
*/
public function isUpdatePrepared() {
- return $this->revision !== null;
+ return $this->revision !== null && $this->revision->getId() !== null;
}
/**
}
/**
- * @return string
- */
- private function getTimestampNow() {
- // TODO: allow an override to be injected for testing
- return wfTimestampNow();
- }
-
- /**
- * Whether the content of the target revision is publicly visible.
+ * Whether the content is deleted and thus not visible to the public.
*
* @return bool
*/
- public function isContentPublic() {
+ public function isContentDeleted() {
if ( $this->revision ) {
- // XXX: if that revision is the current revision, this can be skipped
- return !$this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
+ // XXX: if that revision is the current revision, this should be skipped
+ return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
} else {
- // If the content has not been saved yet, it cannot have been suppressed yet.
- return true;
+ // If the content has not been saved yet, it cannot have been deleted yet.
+ return false;
}
}
return false;
}
- if ( !$this->isContentPublic() ) {
+ if ( $this->isContentDeleted() ) {
// This should be irrelevant: countability only applies to the current revision,
// and the current revision is never suppressed.
return false;
$this->slotsOutput = [];
$this->canonicalParserOutput = null;
- $this->canonicalParserOptions = null;
// The edit may have already been prepared via api.php?action=stashedit
$stashedEdit = false;
$this->slotsUpdate = $slotsUpdate;
if ( $parentRevision ) {
- // start out by inheriting all parent slots
- $this->pstContentSlots = MutableRevisionSlots::newFromParentRevisionSlots(
- $parentRevision->getSlots()->getSlots()
- );
+ $this->revision = MutableRevisionRecord::newFromParentRevision( $parentRevision );
} else {
- $this->pstContentSlots = new MutableRevisionSlots();
+ $this->revision = new MutableRevisionRecord( $title );
}
+ // NOTE: user and timestamp must be set, so they can be used for
+ // {{subst:REVISIONUSER}} and {{subst:REVISIONTIMESTAMP}} in PST!
+ $this->revision->setTimestamp( wfTimestampNow() );
+ $this->revision->setUser( $user );
+
+ // Set up ParserOptions to operate on the new revision
+ $oldCallback = $userPopts->getCurrentRevisionCallback();
+ $userPopts->setCurrentRevisionCallback(
+ function ( Title $parserTitle, $parser = false ) use ( $title, $oldCallback ) {
+ if ( $parserTitle->equals( $title ) ) {
+ $legacyRevision = new Revision( $this->revision );
+ return $legacyRevision;
+ } else {
+ return call_user_func( $oldCallback, $parserTitle, $parser );
+ }
+ }
+ );
+
+ $pstContentSlots = $this->revision->getSlots();
+
foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
$slot = $slotsUpdate->getModifiedSlot( $role );
$pstSlot = SlotRecord::newUnsaved( $role, $pstContent );
}
- $this->pstContentSlots->setSlot( $pstSlot );
+ $pstContentSlots->setSlot( $pstSlot );
}
foreach ( $slotsUpdate->getRemovedRoles() as $role ) {
- $this->pstContentSlots->removeSlot( $role );
+ $pstContentSlots->removeSlot( $role );
}
$this->options['created'] = ( $parentRevision === null );
$this->options['changed'] = ( $parentRevision === null
- || !$this->pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
+ || !$pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
$this->doTransition( 'has-content' );
+
+ if ( !$this->options['changed'] ) {
+ // null-edit!
+
+ // TODO: move this into MutableRevisionRecord
+ // TODO: This needs to behave differently for a forced dummy edit!
+ $this->revision->setId( $parentRevision->getId() );
+ $this->revision->setTimestamp( $parentRevision->getTimestamp() );
+ $this->revision->setPageId( $parentRevision->getPageId() );
+ $this->revision->setParentId( $parentRevision->getParentId() );
+ $this->revision->setUser( $parentRevision->getUser( RevisionRecord::RAW ) );
+ $this->revision->setComment( $parentRevision->getComment( RevisionRecord::RAW ) );
+ $this->revision->setMinorEdit( $parentRevision->isMinor() );
+ $this->revision->setVisibility( $parentRevision->getVisibility() );
+
+ // prepareUpdate() is redundant for null-edits
+ $this->doTransition( 'has-revision' );
+ }
+ }
+
+ /**
+ * Returns the update's target revision - that is, the revision that will be the current
+ * revision after the update.
+ *
+ * @note Callers must treat the returned RevisionRecord's content as immutable, even
+ * if it is a MutableRevisionRecord instance. Other aspects of a MutableRevisionRecord
+ * returned from here, such as the user or the comment, may be changed, but may not
+ * be reflected in ParserOutput until after prepareUpdate() has been called.
+ *
+ * @todo This is currently used by PageUpdater::makeNewRevision() to construct an unsaved
+ * MutableRevisionRecord instance. Introduce something like an UnsavedRevisionFactory service
+ * for that purpose instead!
+ *
+ * @return RevisionRecord
+ */
+ public function getRevision() {
+ $this->assertPrepared( __METHOD__ );
+ return $this->revision;
+ }
+
+ /**
+ * @return RenderedRevision
+ */
+ public function getRenderedRevision() {
+ if ( !$this->renderedRevision ) {
+ $this->assertPrepared( __METHOD__ );
+
+ // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
+ // NOTE: the revision is either new or current, so we can bypass audience checks.
+ $this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
+ $this->revision,
+ null,
+ null,
+ [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ]
+ );
+ }
+
+ return $this->renderedRevision;
}
private function assertHasPageState( $method ) {
}
private function assertPrepared( $method ) {
- if ( !$this->pstContentSlots ) {
+ if ( !$this->revision ) {
throw new LogicException(
'Must call prepareContent() or prepareUpdate() before calling ' . $method
);
}
}
+ private function assertHasRevision( $method ) {
+ if ( !$this->revision->getId() ) {
+ throw new LogicException(
+ 'Must call prepareUpdate() before calling ' . $method
+ );
+ }
+ }
+
/**
* Whether the edit creates the page.
*
/**
* Returns the slots of the target revision, after PST.
*
+ * @note Callers must treat the returned RevisionSlots instance as immutable, even
+ * if it is a MutableRevisionSlots instance.
+ *
* @return RevisionSlots
*/
public function getSlots() {
$this->assertPrepared( __METHOD__ );
- return $this->pstContentSlots;
+ return $this->revision->getSlots();
}
/**
$this->assertPrepared( __METHOD__ );
if ( !$this->slotsUpdate ) {
- if ( !$this->revision ) {
- // This should not be possible: if assertPrepared() returns true,
- // at least one of $this->slotsUpdate or $this->revision should be set.
- throw new LogicException( 'No revision nor a slots update is known!' );
- }
-
$old = $this->getOldRevision();
$this->slotsUpdate = RevisionSlotsUpdate::newFromRevisionSlots(
$this->revision->getSlots(),
* - moved: bool, whether the page was moved (default false)
* - restored: bool, whether the page was undeleted (default false)
* - oldrevision: Revision object for the pre-update revision (default null)
- * - parseroutput: The canonical ParserOutput of $revision (default null)
- * - triggeringuser: The user triggering the update (UserIdentity, default null)
+ * - triggeringUser: The user triggering the update (UserIdentity, defaults to the
+ * user who created the revision)
* - oldredirect: bool, null, or string 'no-change' (default null):
* - bool: whether the page was counted as a redirect before that
* revision, only used in changed is true and created is false
* is true, do update the article count
* - 'no-change': don't update the article count, ever
* When set to null, pageState['oldCountable'] will be used instead if available.
+ * - causeAction: an arbitrary string identifying the reason for the update.
+ * See DataUpdate::getCauseAction(). (default 'unknown')
+ * - causeAgent: name of the user who caused the update. See DataUpdate::getCauseAgent().
+ * (string, default 'unknown')
*/
public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
Assert::parameter(
'must be a RevisionRecord (or Revision)'
);
Assert::parameter(
- !isset( $options['parseroutput'] )
- || $options['parseroutput'] instanceof ParserOutput,
- '$options["parseroutput"]',
- 'must be a ParserOutput'
- );
- Assert::parameter(
- !isset( $options['triggeringuser'] )
- || $options['triggeringuser'] instanceof UserIdentity,
- '$options["triggeringuser"]',
+ !isset( $options['triggeringUser'] )
+ || $options['triggeringUser'] instanceof UserIdentity,
+ '$options["triggeringUser"]',
'must be a UserIdentity'
);
);
}
- if ( $this->revision ) {
+ if ( $this->revision && $this->revision->getId() ) {
if ( $this->revision->getId() === $revision->getId() ) {
return; // nothing to do!
} else {
}
}
- if ( $this->pstContentSlots
- && !$this->pstContentSlots->hasSameContent( $revision->getSlots() )
+ if ( $this->revision
+ && !$this->revision->getSlots()->hasSameContent( $revision->getSlots() )
) {
throw new LogicException(
'The Revision provided has mismatching content!'
$this->options['created'] = ( $this->pageState['oldId'] === 0 );
$this->revision = $revision;
- $this->pstContentSlots = $revision->getSlots();
$this->doTransition( 'has-revision' );
}
// Prune any output that depends on the revision ID.
- if ( $this->canonicalParserOutput ) {
- if ( $this->outputVariesOnRevisionMetaData( $this->canonicalParserOutput, __METHOD__ ) ) {
- $this->canonicalParserOutput = null;
- }
- } else {
- $this->saveParseLogger->debug( __METHOD__ . ": No prepared canonical output...\n" );
- }
-
- if ( $this->slotsOutput ) {
- foreach ( $this->slotsOutput as $role => $prep ) {
- if ( $this->outputVariesOnRevisionMetaData( $prep->output, __METHOD__ ) ) {
- unset( $this->slotsOutput[$role] );
- }
- }
- } else {
- $this->saveParseLogger->debug( __METHOD__ . ": No prepared output...\n" );
- }
-
- // reset ParserOptions, so the actual revision ID is used in future ParserOutput generation
- $this->canonicalParserOptions = null;
-
- // Avoid re-generating the canonical ParserOutput if it's known.
- // We just trust that the caller is passing the correct ParserOutput!
- if ( isset( $options['parseroutput'] ) ) {
- $this->canonicalParserOutput = $options['parseroutput'];
+ if ( $this->renderedRevision ) {
+ $this->renderedRevision->updateRevision( $revision );
}
// TODO: optionally get ParserOutput from the ParserCache here.
// Move the logic used by RefreshLinksJob here!
}
- /**
- * @param ParserOutput $out
- * @param string $method
- * @return bool
- */
- private function outputVariesOnRevisionMetaData( ParserOutput $out, $method = __METHOD__ ) {
- if ( $out->getFlag( 'vary-revision' ) ) {
- // XXX: Just keep the output if the speculative revision ID was correct, like below?
- $this->saveParseLogger->info(
- "$method: Prepared output has vary-revision...\n"
- );
- return true;
- } elseif ( $out->getFlag( 'vary-revision-id' )
- && $out->getSpeculativeRevIdUsed() !== $this->revision->getId()
- ) {
- $this->saveParseLogger->info(
- "$method: Prepared output has vary-revision-id with wrong ID...\n"
- );
- return true;
- } elseif ( $out->getFlag( 'vary-user' )
- && !$this->options['changed']
- ) {
- // When Alice makes a null-edit on top of Bob's edit,
- // {{REVISIONUSER}} must resolve to "Bob", not "Alice", see T135261.
- // TODO: to avoid this, we should check for null-edits in makeCanonicalparserOptions,
- // and set setCurrentRevisionCallback to return the existing revision when appropriate.
- // See also the comment there [dk 2018-05]
- $this->saveParseLogger->info(
- "$method: Prepared output has vary-user and is null-edit...\n"
- );
- return true;
- } else {
- wfDebug( "$method: Keeping prepared output...\n" );
- return false;
- }
- }
-
/**
* @deprecated This only exists for B/C, use the getters on DerivedPageDataUpdater directly!
* @return PreparedEdit
$preparedEdit->popts = $this->getCanonicalParserOptions();
$preparedEdit->output = $this->getCanonicalParserOutput();
- $preparedEdit->pstContent = $this->pstContentSlots->getContent( 'main' );
+ $preparedEdit->pstContent = $this->revision->getContent( 'main' );
$preparedEdit->newContent =
$slotsUpdate->isModifiedSlot( 'main' )
? $slotsUpdate->getModifiedSlot( 'main' )->getContent()
- : $this->pstContentSlots->getContent( 'main' ); // XXX: can we just remove this?
+ : $this->revision->getContent( '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();
return $preparedEdit;
}
- /**
- * @return bool
- */
- private function isContentAccessible() {
- // XXX: when we move this to a RevisionHtmlProvider, the audience may be configurable!
- return $this->isContentPublic();
- }
-
/**
* @param string $role
* @param bool $generateHtml
* @return ParserOutput
*/
public function getSlotParserOutput( $role, $generateHtml = true ) {
- // TODO: factor this out into a RevisionHtmlProvider that can also be used for viewing.
-
- $this->assertPrepared( __METHOD__ );
-
- if ( isset( $this->slotsOutput[$role] ) ) {
- $entry = $this->slotsOutput[$role];
-
- if ( $entry->hasHtml || !$generateHtml ) {
- return $entry->output;
- }
- }
-
- if ( !$this->isContentAccessible() ) {
- // empty output
- $output = new ParserOutput();
- } else {
- $content = $this->getRawContent( $role );
-
- $output = $content->getParserOutput(
- $this->getTitle(),
- $this->revision ? $this->revision->getId() : null,
- $this->getCanonicalParserOptions(),
- $generateHtml
- );
- }
-
- $this->slotsOutput[$role] = (object)[
- 'output' => $output,
- 'hasHtml' => $generateHtml,
- ];
-
- $output->setCacheTime( $this->getTimestampNow() );
-
- return $output;
+ return $this->getRenderedRevision()->getSlotParserOutput(
+ $role,
+ [ 'generate-html' => $generateHtml ]
+ );
}
/**
* @return ParserOutput
*/
public function getCanonicalParserOutput() {
- if ( $this->canonicalParserOutput ) {
- return $this->canonicalParserOutput;
- }
-
- // TODO: MCR: logic for combining the output of multiple slot goes here!
- // TODO: factor this out into a RevisionHtmlProvider that can also be used for viewing.
- $this->canonicalParserOutput = $this->getSlotParserOutput( 'main' );
-
- return $this->canonicalParserOutput;
+ return $this->getRenderedRevision()->getRevisionParserOutput();
}
/**
* @return ParserOptions
*/
public function getCanonicalParserOptions() {
- if ( $this->canonicalParserOptions ) {
- return $this->canonicalParserOptions;
- }
-
- // TODO: ParserOptions should *not* be controlled by the ContentHandler!
- // See T190712 for how to fix this for Wikibase.
- $this->canonicalParserOptions = $this->wikiPage->makeParserOptions( 'canonical' );
-
- //TODO: if $this->revision is not set but we already know that we pending update is a
- // null-edit, we should probably use the page's current revision here.
- // That would avoid the need for the !$this->options['changed'] branch in
- // outputVariesOnRevisionMetaData [dk 2018-05]
-
- if ( $this->revision ) {
- // Make sure we use the appropriate revision ID when generating output
- $title = $this->getTitle();
- $oldCallback = $this->canonicalParserOptions->getCurrentRevisionCallback();
- $this->canonicalParserOptions->setCurrentRevisionCallback(
- function ( Title $parserTitle, $parser = false ) use ( $title, &$oldCallback ) {
- if ( $parserTitle->equals( $title ) ) {
- $legacyRevision = new Revision( $this->revision );
- return $legacyRevision;
- } else {
- return call_user_func( $oldCallback, $parserTitle, $parser );
- }
- }
- );
- } else {
- // NOTE: we only get here without READ_LATEST if called directly by application logic
- $dbIndex = $this->useMaster()
- ? DB_MASTER // use the best possible guess
- : DB_REPLICA; // T154554
-
- $this->canonicalParserOptions->setSpeculativeRevIdCallback(
- function () use ( $dbIndex ) {
- // TODO: inject LoadBalancer!
- $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
- // Use a fresh connection in order to see the latest data, by avoiding
- // stale data from REPEATABLE-READ snapshots.
- // HACK: But don't use a fresh connection in unit tests, since it would not have
- // the fake tables. This should be handled by the LoadBalancer!
- $flags = defined( 'MW_PHPUNIT_TEST' ) ? 0 : $lb::CONN_TRX_AUTOCOMMIT;
- $db = $lb->getConnectionRef( $dbIndex, [], $this->getWikiId(), $flags );
-
- return 1 + (int)$db->selectField(
- 'revision',
- 'MAX(rev_id)',
- [],
- __METHOD__
- );
- }
- );
- }
-
- return $this->canonicalParserOptions;
+ return $this->getRenderedRevision()->getOptions();
}
/**
$wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
- // NOTE: this may trigger the first parsing of the new content after an edit (when not
- // using pre-generated stashed output).
- // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
- // to be perform post-send. The client could already follow a HTTP redirect to the
- // page view, but would then have to wait for a response until rendering is complete.
- $output = $this->getCanonicalParserOutput();
-
- // Save it to the parser cache.
- // Make sure the cache time matches page_touched to avoid double parsing.
- $this->parserCache->save(
- $output, $wikiPage, $this->getCanonicalParserOptions(),
- $this->revision->getTimestamp(), $this->revision->getId()
- );
-
$legacyUser = User::newFromIdentity( $this->user );
$legacyRevision = new Revision( $this->revision );
- // Update the links tables and other secondary data
- $recursive = $this->options['changed']; // T52785
- $updates = $this->getSecondaryDataUpdates( $recursive );
+ $this->doParserCacheUpdate();
- foreach ( $updates as $update ) {
- // TODO: make an $option field for the cause
- $update->setCause( 'edit-page', $this->user->getName() );
- if ( $update instanceof LinksUpdate ) {
- $update->setRevision( $legacyRevision );
-
- if ( !empty( $this->options['triggeringuser'] ) ) {
- /** @var UserIdentity|User $triggeringUser */
- $triggeringUser = $this->options['triggeringuser'];
- if ( !$triggeringUser instanceof User ) {
- $triggeringUser = User::newFromIdentity( $triggeringUser );
- }
-
- $update->setTriggeringUser( $triggeringUser );
- }
- }
- DeferredUpdates::addUpdate( $update );
- }
+ $this->doSecondaryDataUpdates( [
+ // T52785 do not update any other pages on a null edit
+ 'recursive' => $this->options['changed'],
+ 'defer' => DeferredUpdates::POSTSEND,
+ ] );
// TODO: MCR: check if *any* changed slot supports categories!
if ( $this->rcWatchCategoryMembership
// TODO: make search infrastructure aware of slots!
$mainSlot = $this->revision->getSlot( 'main' );
- if ( !$mainSlot->isInherited() && $this->isContentPublic() ) {
+ if ( !$mainSlot->isInherited() && !$this->isContentDeleted() ) {
DeferredUpdates::addUpdate( new SearchUpdate( $id, $dbKey, $mainSlot->getContent() ) );
}
if ( $title->getNamespace() == NS_MEDIAWIKI
&& $this->getRevisionSlotsUpdate()->isModifiedSlot( 'main' )
) {
- $mainContent = $this->isContentPublic() ? $this->getRawContent( 'main' ) : null;
+ $mainContent = $this->isContentDeleted() ? null : $this->getRawContent( 'main' );
$this->messageCache->updateMessageOverride( $title, $mainContent );
}
$this->doTransition( 'done' );
}
+ /**
+ * Do secondary data updates (such as updating link tables).
+ *
+ * MCR note: this method is temporarily exposed via WikiPage::doSecondaryDataUpdates.
+ *
+ * @param array $options
+ * - recursive: make the update recursive, i.e. also update pages which transclude the
+ * current page or otherwise depend on it (default: false)
+ * - defer: one of the DeferredUpdates constants, or false to run immediately after waiting
+ * for replication of the changes from the SecondaryDataUpdates hooks (default: false)
+ * - transactionTicket: a transaction ticket from LBFactory::getEmptyTransactionTicket(),
+ * only when defer is false (default: null)
+ * @since 1.32
+ */
+ public function doSecondaryDataUpdates( array $options = [] ) {
+ $this->assertHasRevision( __METHOD__ );
+ $options += [
+ 'recursive' => false,
+ 'defer' => false,
+ 'transactionTicket' => null,
+ ];
+ $deferValues = [ false, DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND ];
+ if ( !in_array( $options['defer'], $deferValues, true ) ) {
+ throw new InvalidArgumentException( 'invalid value for defer: ' . $options['defer'] );
+ }
+ Assert::parameterType( 'integer|null', $options['transactionTicket'],
+ '$options[\'transactionTicket\']' );
+
+ $updates = $this->getSecondaryDataUpdates( $options['recursive'] );
+
+ $triggeringUser = $this->options['triggeringUser'] ?? $this->user;
+ if ( !$triggeringUser instanceof User ) {
+ $triggeringUser = User::newFromIdentity( $triggeringUser );
+ }
+ $causeAction = $this->options['causeAction'] ?? 'unknown';
+ $causeAgent = $this->options['causeAgent'] ?? 'unknown';
+ $legacyRevision = new Revision( $this->revision );
+
+ if ( $options['defer'] === false && $options['transactionTicket'] !== null ) {
+ // For legacy hook handlers doing updates via LinksUpdateConstructed, make sure
+ // any pending writes they made get flushed before the doUpdate() calls below.
+ // This avoids snapshot-clearing errors in LinksUpdate::acquirePageLock().
+ $this->loadbalancerFactory->commitAndWaitForReplication(
+ __METHOD__, $options['transactionTicket']
+ );
+ }
+
+ foreach ( $updates as $update ) {
+ $update->setCause( $causeAction, $causeAgent );
+ if ( $update instanceof LinksUpdate ) {
+ $update->setRevision( $legacyRevision );
+ $update->setTriggeringUser( $triggeringUser );
+ }
+ if ( $options['defer'] === false ) {
+ if ( $options['transactionTicket'] !== null ) {
+ $update->setTransactionTicket( $options['transactionTicket'] );
+ }
+ $update->doUpdate();
+ } else {
+ DeferredUpdates::addUpdate( $update, $options['defer'] );
+ }
+ }
+ }
+
+ public function doParserCacheUpdate() {
+ $this->assertHasRevision( __METHOD__ );
+
+ $wikiPage = $this->getWikiPage(); // TODO: ParserCache should accept a RevisionRecord instead
+
+ // NOTE: this may trigger the first parsing of the new content after an edit (when not
+ // using pre-generated stashed output).
+ // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
+ // to be performed post-send. The client could already follow a HTTP redirect to the
+ // page view, but would then have to wait for a response until rendering is complete.
+ $output = $this->getCanonicalParserOutput();
+
+ // Save it to the parser cache. Use the revision timestamp in the case of a
+ // freshly saved edit, as that matches page_touched and a mismatch would trigger an
+ // unnecessary reparse.
+ $timestamp = $this->options['changed'] ? $this->revision->getTimestamp()
+ : $output->getTimestamp();
+ $this->parserCache->save(
+ $output, $wikiPage, $this->getCanonicalParserOptions(),
+ $timestamp, $this->revision->getId()
+ );
+ }
+
}