Merge "RevisionStoreDbTestBase, remove redundant needsDB override"
[lhc/web/wiklou.git] / includes / page / WikiPage.php
index 147c9f3..74e3179 100644 (file)
 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;
        }
 
        /**