X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Fpage%2FWikiPage.php;h=74e31791e9e302cea2811275f578f4ccf0d37bea;hp=147c9f311dc7b548f0fbbbecea3f7005014df2d7;hb=74d04edec385aa86ee01943b9a27475d79f74e78;hpb=15f6eff90c305d405fe4331c8a8dc8caa842e5b3 diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 147c9f311d..74e31791e9 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -23,11 +23,13 @@ use MediaWiki\Edit\PreparedEdit; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; +use MediaWiki\Revision\RevisionRenderer; use MediaWiki\Storage\DerivedPageDataUpdater; use MediaWiki\Storage\PageUpdater; use MediaWiki\Storage\RevisionRecord; use MediaWiki\Storage\RevisionSlotsUpdate; use MediaWiki\Storage\RevisionStore; +use MediaWiki\Storage\SlotRecord; use Wikimedia\Assert\Assert; use Wikimedia\Rdbms\FakeResultWrapper; use Wikimedia\Rdbms\IDatabase; @@ -223,6 +225,13 @@ class WikiPage implements Page, IDBAccessObject { return MediaWikiServices::getInstance()->getRevisionStore(); } + /** + * @return RevisionRenderer + */ + private function getRevisionRenderer() { + return MediaWikiServices::getInstance()->getRevisionRenderer(); + } + /** * @return ParserCache */ @@ -761,6 +770,18 @@ class WikiPage implements Page, IDBAccessObject { return null; } + /** + * Get the latest revision + * @return RevisionRecord|null + */ + public function getRevisionRecord() { + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision->getRevisionRecord(); + } + return null; + } + /** * Get the content of the current revision. No side-effects... * @@ -931,6 +952,7 @@ class WikiPage implements Page, IDBAccessObject { // links. $hasLinks = (bool)count( $editInfo->output->getLinks() ); } else { + // NOTE: keep in sync with revisionRenderer::getLinkCount $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1, [ 'pl_from' => $this->getId() ], __METHOD__ ); } @@ -1155,6 +1177,8 @@ class WikiPage implements Page, IDBAccessObject { * The parser cache will be used if possible. Cache misses that result * in parser runs are debounced with PoolCounter. * + * XXX merge this with updateParserCache()? + * * @since 1.19 * @param ParserOptions $parserOptions ParserOptions to use for the parse operation * @param null|int $oldid Revision ID to get the text from, passing null or 0 will @@ -1630,11 +1654,12 @@ class WikiPage implements Page, IDBAccessObject { $derivedDataUpdater = new DerivedPageDataUpdater( $this, // NOTE: eventually, PageUpdater should not know about WikiPage $this->getRevisionStore(), + $this->getRevisionRenderer(), $this->getParserCache(), JobQueueGroup::singleton(), MessageCache::singleton(), MediaWikiServices::getInstance()->getContentLanguage(), - LoggerFactory::getInstance( 'SaveParse' ) + MediaWikiServices::getInstance()->getDBLoadBalancerFactory() ); $derivedDataUpdater->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership ); @@ -1948,7 +1973,13 @@ class WikiPage implements Page, IDBAccessObject { $updater->prepareContent( $user, $slots, $useCache ); if ( $revision ) { - $updater->prepareUpdate( $revision ); + $updater->prepareUpdate( + $revision, + [ + 'causeAction' => 'prepare-edit', + 'causeAgent' => $user->getName(), + ] + ); } } @@ -1961,7 +1992,7 @@ class WikiPage implements Page, IDBAccessObject { * Purges pages that include this page if the text was changed here. * Every 100th edit, prune the recent changes table. * - * @deprecated since 1.32, use PageUpdater::doEditUpdates instead. + * @deprecated since 1.32, use PageUpdater::doUpdates instead. * * @param Revision $revision * @param User $user User object that did the revision @@ -1977,8 +2008,17 @@ class WikiPage implements Page, IDBAccessObject { * - null: if created is false, don't update the article count; if created * is true, do update the article count * - 'no-change': don't update the article count, ever + * - causeAction: an arbitrary string identifying the reason for the update. + * See DataUpdate::getCauseAction(). (default 'edit-page') + * - causeAgent: name of the user who caused the update. See DataUpdate::getCauseAgent(). + * (string, defaults to the passed user) */ public function doEditUpdates( Revision $revision, User $user, array $options = [] ) { + $options += [ + 'causeAction' => 'edit-page', + 'causeAgent' => $user->getName(), + ]; + $revision = $revision->getRevisionRecord(); $updater = $this->getDerivedDataUpdater( $user, $revision ); @@ -1988,6 +2028,76 @@ class WikiPage implements Page, IDBAccessObject { $updater->doUpdates(); } + /** + * Update the parser cache. + * + * @note This is a temporary workaround until there is a proper data updater class. + * It will become deprecated soon. + * + * @param array $options + * - causeAction: an arbitrary string identifying the reason for the update. + * See DataUpdate::getCauseAction(). (default 'edit-page') + * - causeAgent: name of the user who caused the update (string, defaults to the + * user who created the revision) + * @since 1.32 + */ + public function updateParserCache( array $options = [] ) { + $revision = $this->getRevisionRecord(); + if ( !$revision || !$revision->getId() ) { + LoggerFactory::getInstance( 'wikipage' )->info( + __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision' + ); + return; + } + $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) ); + + $updater = $this->getDerivedDataUpdater( $user, $revision ); + $updater->prepareUpdate( $revision, $options ); + $updater->doParserCacheUpdate(); + } + + /** + * Do secondary data updates (such as updating link tables). + * Secondary data updates are only a small part of the updates needed after saving + * a new revision; normally PageUpdater::doUpdates should be used instead (which includes + * secondary data updates). This method is provided for partial purges. + * + * @note This is a temporary workaround until there is a proper data updater class. + * It will become deprecated soon. + * + * @param array $options + * - recursive (bool, default true): whether to do a recursive update (update pages that + * depend on this page, e.g. transclude it). This will set the $recursive parameter of + * Content::getSecondaryDataUpdates. Typically this should be true unless the update + * was something that did not really change the page, such as a null edit. + * - triggeringUser: The user triggering the update (UserIdentity, defaults to the + * user who created the revision) + * - causeAction: an arbitrary string identifying the reason for the update. + * See DataUpdate::getCauseAction(). (default 'unknown') + * - causeAgent: name of the user who caused the update (string, default 'unknown') + * - defer: one of the DeferredUpdates constants, or false to run immediately (default: false). + * Note that even when this is set to false, some updates might still get deferred (as + * some update might directly add child updates to DeferredUpdates). + * - transactionTicket: a transaction ticket from LBFactory::getEmptyTransactionTicket(), + * only when defer is false (default: null) + * @since 1.32 + */ + public function doSecondaryDataUpdates( array $options = [] ) { + $options['recursive'] = $options['recursive'] ?? true; + $revision = $this->getRevisionRecord(); + if ( !$revision || !$revision->getId() ) { + LoggerFactory::getInstance( 'wikipage' )->info( + __METHOD__ . 'called with ' . ( $revision ? 'unsaved' : 'no' ) . ' revision' + ); + return; + } + $user = User::newFromIdentity( $revision->getUser( RevisionRecord::RAW ) ); + + $updater = $this->getDerivedDataUpdater( $user, $revision ); + $updater->prepareUpdate( $revision, $options ); + $updater->doSecondaryDataUpdates( $options ); + } + /** * Update the article's restriction field, and leave a log entry. * This works for protection both existing and non-existing pages. @@ -2711,15 +2821,21 @@ class WikiPage implements Page, IDBAccessObject { * Do some database updates after deletion * * @param int $id The page_id value of the page being deleted - * @param Content|null $content Optional page content to be used when determining + * @param Content|null $content Page content to be used when determining * the required updates. This may be needed because $this->getContent() * may already return null when the page proper was deleted. - * @param Revision|null $revision The latest page revision + * @param RevisionRecord|Revision|null $revision The current page revision at the time of + * deletion, used when determining the required updates. This may be needed because + * $this->getRevision() may already return null when the page proper was deleted. * @param User|null $user The user that caused the deletion */ public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null, User $user = null ) { + if ( $id !== $this->getId() ) { + throw new InvalidArgumentException( 'Mismatching page ID' ); + } + try { $countable = $this->isCountable(); } catch ( Exception $ex ) { @@ -2734,7 +2850,9 @@ class WikiPage implements Page, IDBAccessObject { ) ); // Delete pagelinks, update secondary indexes, etc - $updates = $this->getDeletionUpdates( $content ); + $updates = $this->getDeletionUpdates( + $revision ? $revision->getRevisionRecord() : $content + ); foreach ( $updates as $update ) { DeferredUpdates::addUpdate( $update ); } @@ -3152,6 +3270,9 @@ class WikiPage implements Page, IDBAccessObject { // Image redirects RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); + + // Purge cross-wiki cache entities referencing this page + self::purgeInterwikiCheckKey( $title ); } /** @@ -3190,14 +3311,41 @@ class WikiPage implements Page, IDBAccessObject { // Clear file cache for this page only HTMLFileCache::clearFileCache( $title ); + // Purge ?action=info cache $revid = $revision ? $revision->getId() : null; DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) { InfoAction::invalidateCache( $title, $revid ); } ); + + // Purge cross-wiki cache entities referencing this page + self::purgeInterwikiCheckKey( $title ); } /**#@-*/ + /** + * Purge the check key for cross-wiki cache entries referencing this page + * + * @param Title $title + */ + private static function purgeInterwikiCheckKey( Title $title ) { + global $wgEnableScaryTranscluding; + + if ( !$wgEnableScaryTranscluding ) { + return; // @todo: perhaps this wiki is only used as a *source* for content? + } + + DeferredUpdates::addCallableUpdate( function () use ( $title ) { + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + $cache->resetCheckKey( + // Do not include the namespace since there can be multiple aliases to it + // due to different namespace text definitions on different wikis. This only + // means that some cache invalidations happen that are not strictly needed. + $cache->makeGlobalKey( 'interwiki-page', wfWikiID(), $title->getDBkey() ) + ); + } ); + } + /** * Returns a list of categories this page is a member of. * Results will include hidden categories @@ -3406,32 +3554,68 @@ class WikiPage implements Page, IDBAccessObject { * updates should remove any information about this page from secondary data * stores such as links tables. * - * @param Content|null $content Optional Content object for determining the - * necessary updates. + * @param RevisionRecord|Content|null $rev The revision being deleted. Also accepts a Content + * object for backwards compatibility. * @return DeferrableUpdate[] */ - public function getDeletionUpdates( Content $content = null ) { - if ( !$content ) { - // load content object, which may be used to determine the necessary updates. - // XXX: the content may not be needed to determine the updates. + public function getDeletionUpdates( $rev = null ) { + if ( !$rev ) { + wfDeprecated( __METHOD__ . ' without a RevisionRecord', '1.32' ); + try { - $content = $this->getContent( Revision::RAW ); + $rev = $this->getRevisionRecord(); } catch ( Exception $ex ) { // If we can't load the content, something is wrong. Perhaps that's why // the user is trying to delete the page, so let's not fail in that case. // Note that doDeleteArticleReal() will already have logged an issue with // loading the content. + wfDebug( __METHOD__ . ' failed to load current revision of page ' . $this->getId() ); } } - if ( !$content ) { - $updates = []; + if ( !$rev ) { + $slotContent = []; + } elseif ( $rev instanceof Content ) { + wfDeprecated( __METHOD__ . ' with a Content object instead of a RevisionRecord', '1.32' ); + + $slotContent = [ 'main' => $rev ]; } else { - $updates = $content->getDeletionUpdates( $this ); + $slotContent = array_map( function ( SlotRecord $slot ) { + return $slot->getContent( Revision::RAW ); + }, $rev->getSlots()->getSlots() ); } - Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] ); - return $updates; + $allUpdates = [ new LinksDeletionUpdate( $this ) ]; + + // NOTE: once Content::getDeletionUpdates() is removed, we only need to content + // model here, not the content object! + // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates() + /** @var Content $content */ + foreach ( $slotContent as $role => $content ) { + $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( $this ); + + // HACK: filter out redundant and incomplete LinksDeletionUpdate + $legacyUpdates = array_filter( $legacyUpdates, function ( $update ) { + return !( $update instanceof LinksDeletionUpdate ); + } ); + + $allUpdates = array_merge( $allUpdates, $legacyUpdates ); + } + + Hooks::run( 'PageDeletionDataUpdates', [ $this->getTitle(), $rev, &$allUpdates ] ); + + // TODO: hard deprecate old hook in 1.33 + Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$allUpdates ] ); + return $allUpdates; } /**