Merge "Remove xx-uca-et collation workaround"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 11 Sep 2018 18:13:00 +0000 (18:13 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 11 Sep 2018 18:13:00 +0000 (18:13 +0000)
26 files changed:
docs/hooks.txt
includes/DefaultSettings.php
includes/Revision.php
includes/Revision/RenderedRevision.php
includes/Storage/RevisionArchiveRecord.php
includes/Storage/RevisionRecord.php
includes/Storage/RevisionStore.php
includes/Storage/RevisionStoreRecord.php
includes/content/ContentHandler.php
includes/diff/DifferenceEngine.php
includes/filerepo/file/LocalFile.php
includes/jobqueue/jobs/CategoryMembershipChangeJob.php
includes/page/Article.php
includes/page/ImagePage.php
includes/poolcounter/PoolWorkArticleView.php
includes/specials/SpecialUndelete.php
maintenance/compareParserCache.php
tests/phpunit/includes/Revision/RenderedRevisionTest.php
tests/phpunit/includes/Revision/RevisionRendererTest.php
tests/phpunit/includes/Storage/MutableRevisionRecordTest.php
tests/phpunit/includes/Storage/RevisionRecordTests.php
tests/phpunit/includes/Storage/RevisionStoreTest.php
tests/phpunit/includes/content/TextContentTest.php
tests/phpunit/includes/content/WikitextContentTest.php
tests/phpunit/includes/page/ArticleViewTest.php
tests/phpunit/includes/poolcounter/PoolWorkArticleViewTest.php [new file with mode: 0644]

index 436131c..cce50e0 100644 (file)
@@ -624,8 +624,8 @@ a chance to hide their (unrelated) log entries.
   AND in the final query)
 $logTypes: Array of log types being queried
 
-'ArticleAfterFetchContentObject': After fetching content of an article from the
-database.
+'ArticleAfterFetchContentObject': DEPRECATED since 1.32, use ArticleRevisionViewCustom
+to control output. After fetching content of an article from the database.
 &$article: the article (object) being loaded from the database
 &$content: the content of the article, as a Content object
 
@@ -640,12 +640,21 @@ this to change the content in this area or how it is loaded.
 $diffEngine: the DifferenceEngine
 $output: the OutputPage object
 
-'ArticleContentViewCustom': Allows to output the text of the article in a
-different format than wikitext. Note that it is preferable to implement proper
-handing for a custom data type using the ContentHandler facility.
+'ArticleRevisionViewCustom': Allows custom rendering of an article's content.
+Note that it is preferable to implement proper handing for a custom data type using
+the ContentHandler facility.
+$revision: content of the page, as a RevisionRecord object, or null if the revision
+  could not be loaded. May also be a fake that wraps content supplied by an extension.
+$title: title of the page
+$oldid: the requested revision id, or 0 for the currrent revision.
+$output: a ParserOutput object
+
+'ArticleContentViewCustom': DEPRECATED since 1.32, use ArticleRevisionViewCustom instead,
+or provide an appropriate ContentHandler. Allows to output the text of the article in a
+different format than wikitext.
 $content: content of the page, as a Content object
 $title: title of the page
-$output: reference to $wgOut
+$output: a ParserOutput object
 
 'ArticleDelete': Before an article is deleted.
 &$wikiPage: the WikiPage (object) being deleted
@@ -775,8 +784,8 @@ $article: the article
 $article: Article object
 $patrolFooterShown: boolean whether patrol footer is shown
 
-'ArticleViewHeader': Before the parser cache is about to be tried for article
-viewing.
+'ArticleViewHeader': Control article output. Called before the parser cache is about
+to be tried for article viewing.
 &$article: the article
 &$pcache: whether to try the parser cache or not
 &$outputDone: whether the output for this page finished or not. Set to
index 702ea54..343e80d 100644 (file)
@@ -8998,7 +8998,7 @@ $wgCommentTableSchemaMigrationStage = MIGRATION_OLD;
  * @since 1.32
  * @var int An appropriate combination of SCHEMA_COMPAT_XXX flags.
  */
-$wgMultiContentRevisionSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD;
+$wgMultiContentRevisionSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW;
 
 /**
  * Actor table schema migration stage.
index 6d684a8..1e35dda 100644 (file)
@@ -350,6 +350,7 @@ class Revision implements IDBAccessObject {
         */
        public static function selectFields() {
                global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage;
+               global $wgMultiContentRevisionSchemaMigrationStage;
 
                if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
                        // If code is using this instead of self::getQueryInfo(), there's a
@@ -361,6 +362,18 @@ class Revision implements IDBAccessObject {
                        );
                }
 
+               if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
+                       // If code is using this instead of self::getQueryInfo(), there's a
+                       // decent chance it's going to try to directly access
+                       // $row->rev_text_id or $row->rev_content_model and we can't give it
+                       // useful values here once those aren't being written anymore,
+                       // and may not exist at all.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgMultiContentRevisionSchemaMigrationStage '
+                               . 'does not have SCHEMA_COMPAT_WRITE_OLD set.'
+                       );
+               }
+
                wfDeprecated( __METHOD__, '1.31' );
 
                $fields = [
@@ -396,6 +409,7 @@ class Revision implements IDBAccessObject {
         */
        public static function selectArchiveFields() {
                global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage;
+               global $wgMultiContentRevisionSchemaMigrationStage;
 
                if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
                        // If code is using this instead of self::getQueryInfo(), there's a
@@ -407,6 +421,18 @@ class Revision implements IDBAccessObject {
                        );
                }
 
+               if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
+                       // If code is using this instead of self::getQueryInfo(), there's a
+                       // decent chance it's going to try to directly access
+                       // $row->ar_text_id or $row->ar_content_model and we can't give it
+                       // useful values here once those aren't being written anymore,
+                       // and may not exist at all.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgMultiContentRevisionSchemaMigrationStage '
+                               . 'does not have SCHEMA_COMPAT_WRITE_OLD set.'
+                       );
+               }
+
                wfDeprecated( __METHOD__, '1.31' );
 
                $fields = [
index 0c052d1..ba40a81 100644 (file)
@@ -95,7 +95,11 @@ class RenderedRevision {
         * but should use a RevisionRenderer instead.
         *
         * @param Title $title
-        * @param RevisionRecord $revision
+        * @param RevisionRecord $revision The revision to render. The content for rendering will be
+        *        taken from this RevisionRecord. However, if the RevisionRecord is not complete
+        *        according isReadyForInsertion(), but a revision ID is known, the parser may load
+        *        the revision from the database if it needs revision meta data to handle magic
+        *        words like {{REVISIONUSER}}.
         * @param ParserOptions $options
         * @param callable $combineOutput Callback for combining slot output into revision output.
         *        Signature: function ( RenderedRevision $this ): ParserOutput.
@@ -287,19 +291,50 @@ class RenderedRevision {
        private function setRevisionInternal( RevisionRecord $revision ) {
                $this->revision = $revision;
 
-               // Make sure the parser uses the correct Revision object
-               $title = $this->title;
-               $oldCallback = $this->options->getCurrentRevisionCallback();
-               $this->options->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 );
+               // Force the parser to use  $this->revision to resolve magic words like {{REVISIONUSER}}
+               // if the revision is either known to be complete, or it doesn't have a revision ID set.
+               // If it's incomplete and we have a revision ID, the parser can do better by loading
+               // the revision from the database if needed to handle a magic word.
+               //
+               // The following considerations inform the logic described above:
+               //
+               // 1) If we have a saved revision already loaded, we want the parser to use it, instead of
+               // loading it again.
+               //
+               // 2) If the revision is a fake that wraps some kind of synthetic content, such as an
+               // error message from Article, it should be used directly and things like {{REVISIONUSER}}
+               // should not expected to work, since there may not even be an actual revision to
+               // refer to.
+               //
+               // 3) If the revision is a fake constructed around a Title, a Content object, and
+               // a revision ID, to provide backwards compatibility to code that has access to those
+               // but not to a complete RevisionRecord for rendering, then we want the Parser to
+               // load the actual revision from the database when it encounters a magic word like
+               // {{REVISIONUSER}}, but we don't want to load that revision ahead of time just in case.
+               //
+               // 4) Previewing an edit to a template should use the submitted unsaved
+               // MutableRevisionRecord for self-transclusions in the template's documentation (see T7278).
+               // That revision would be complete except for the ID field.
+               //
+               // 5) Pre-save transform would provide a RevisionRecord that has all meta-data but is
+               // incomplete due to not yet having content set. However, since it doesn't have a revision
+               // ID either, the below code would still force it to be used, allowing
+               // {{subst::REVISIONUSER}} to function as expected.
+
+               if ( $this->revision->isReadyForInsertion() || !$this->revision->getId() ) {
+                       $title = $this->title;
+                       $oldCallback = $this->options->getCurrentRevisionCallback();
+                       $this->options->setCurrentRevisionCallback(
+                               function ( Title $parserTitle, $parser = false ) use ( $title, $oldCallback ) {
+                                       if ( $title->equals( $parserTitle ) ) {
+                                               $legacyRevision = new Revision( $this->revision );
+                                               return $legacyRevision;
+                                       } else {
+                                               return call_user_func( $oldCallback, $parserTitle, $parser );
+                                       }
                                }
-                       }
-               );
+                       );
+               }
        }
 
        /**
index 213ee3c..173da51 100644 (file)
@@ -167,4 +167,13 @@ class RevisionArchiveRecord extends RevisionRecord {
                return parent::getTimestamp();
        }
 
+       /**
+        * @see RevisionStore::isComplete
+        *
+        * @return bool always true.
+        */
+       public function isReadyForInsertion() {
+               return true;
+       }
+
 }
index 17c56ea..8c31a3c 100644 (file)
@@ -532,4 +532,29 @@ abstract class RevisionRecord {
                }
        }
 
+       /**
+        * Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all
+        * information needed to save it to the database. This should trivially be true for
+        * RevisionRecords loaded from the database.
+        *
+        * Note that this may return true even if getId() or getPage() return null or 0, since these
+        * are generally assigned while the revision is saved to the database, and may not be available
+        * before.
+        *
+        * @return bool
+        */
+       public function isReadyForInsertion() {
+               // NOTE: don't check getSize() and getSha1(), since that may cause the full content to
+               // be loaded in order to calculate the values. Just assume these methods will not return
+               // null if mSlots is not empty.
+
+               // NOTE: getId() and getPageId() may return null before a revision is saved, so don't
+               //check them.
+
+               return $this->getTimestamp() !== null
+                       && $this->getComment( self::RAW ) !== null
+                       && $this->getUser( self::RAW ) !== null
+                       && $this->mSlots->getSlotRoles() !== [];
+       }
+
 }
index ce8a088..8e66906 100644 (file)
@@ -466,6 +466,12 @@ class RevisionStore
                $this->failOnNull( $user->getId(), 'user field' );
                $this->failOnEmpty( $user->getName(), 'user_text field' );
 
+               if ( !$rev->isReadyForInsertion() ) {
+                       // This is here for future-proofing. At the time this check being added, it
+                       // was redundant to the individual checks above.
+                       throw new IncompleteRevisionException( 'Revision is incomplete' );
+               }
+
                // TODO: we shouldn't need an actual Title here.
                $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
                $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
index d092f22..6148c44 100644 (file)
@@ -207,4 +207,13 @@ class RevisionStoreRecord extends RevisionRecord {
                return parent::getTimestamp();
        }
 
+       /**
+        * @see RevisionStore::isComplete
+        *
+        * @return bool always true.
+        */
+       public function isReadyForInsertion() {
+               return true;
+       }
+
 }
index 344d040..7a378b3 100644 (file)
@@ -1389,14 +1389,20 @@ abstract class ContentHandler {
         * @return ParserOutput
         */
        public function getParserOutputForIndexing( WikiPage $page, ParserCache $cache = null ) {
+               // TODO: MCR: ContentHandler should be called per slot, not for the whole page.
+               // See T190066.
                $parserOptions = $page->makeParserOptions( 'canonical' );
-               $revId = $page->getRevision()->getId();
                if ( $cache ) {
                        $parserOutput = $cache->get( $page, $parserOptions );
                }
+
                if ( empty( $parserOutput ) ) {
+                       $renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
                        $parserOutput =
-                               $page->getContent()->getParserOutput( $page->getTitle(), $revId, $parserOptions );
+                               $renderer->getRenderedRevision(
+                                       $page->getRevision()->getRevisionRecord(),
+                                       $parserOptions
+                               )->getRevisionParserOutput();
                        if ( $cache ) {
                                $cache->save( $parserOutput, $page, $parserOptions );
                        }
index 1d69f12..387e9e3 100644 (file)
@@ -822,8 +822,20 @@ class DifferenceEngine extends ContextSource {
 
        /**
         * Show the new revision of the page.
+        *
+        * @note Not supported after calling setContent().
         */
        public function renderNewRevision() {
+               if ( $this->isContentOverridden ) {
+                       // The code below only works with a Revision object. We could construct a fake revision
+                       // (here or in setContent), but since this does not seem needed at the moment,
+                       // we'll just fail for now.
+                       throw new LogicException(
+                               __METHOD__
+                               . ' is not supported after calling setContent(). Use setRevisions() instead.'
+                       );
+               }
+
                $out = $this->getOutput();
                $revHeader = $this->getRevisionHeader( $this->mNewRev );
                # Add "current version as of X" title
@@ -842,10 +854,16 @@ class DifferenceEngine extends ContextSource {
                        $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
                        $out->setArticleFlag( true );
 
-                       if ( !Hooks::run( 'ArticleContentViewCustom',
-                               [ $this->mNewContent, $this->mNewPage, $out ] )
+                       if ( !Hooks::run( 'ArticleRevisionViewCustom',
+                               [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $out ] )
                        ) {
                                // Handled by extension
+                               // NOTE: sync with hooks called in Article::view()
+                       } elseif ( !Hooks::run( 'ArticleContentViewCustom',
+                               [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
+                       ) {
+                               // Handled by extension
+                               // NOTE: sync with hooks called in Article::view()
                        } else {
                                // Normal page
                                if ( $this->getTitle()->equals( $this->mNewPage ) ) {
@@ -889,6 +907,13 @@ class DifferenceEngine extends ContextSource {
         * @return ParserOutput|bool False if the revision was not found
         */
        protected function getParserOutput( WikiPage $page, Revision $rev ) {
+               if ( !$rev->getId() ) {
+                       // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
+                       // the current revision, so fail instead. If need be, WikiPage::getParserOutput
+                       // could be made to accept a Revision or RevisionRecord instead of the id.
+                       return false;
+               }
+
                $parserOptions = $page->makeParserOptions( $this->getContext() );
                $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
 
index 7920e9c..fa6e180 100644 (file)
@@ -2116,16 +2116,21 @@ class LocalFile extends File {
         * @return string|false
         */
        function getDescriptionText( Language $lang = null ) {
-               $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revision = $store->getRevisionByTitle( $this->title, 0, Revision::READ_NORMAL );
                if ( !$revision ) {
                        return false;
                }
-               $content = $revision->getContent();
-               if ( !$content ) {
+
+               $renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
+               $rendered = $renderer->getRenderedRevision( $revision, new ParserOptions( null, $lang ) );
+
+               if ( !$rendered ) {
+                       // audience check failed
                        return false;
                }
-               $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
 
+               $pout = $rendered->getRevisionParserOutput();
                return $pout->getText();
        }
 
index a0c70ab..c39823f 100644 (file)
@@ -232,12 +232,14 @@ class CategoryMembershipChangeJob extends Job {
         * @return string[] category names
         */
        private function getCategoriesAtRev( WikiPage $page, Revision $rev, $parseTimestamp ) {
-               $content = $rev->getContent();
+               $renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
                $options = $page->makeParserOptions( 'canonical' );
                $options->setTimestamp( $parseTimestamp );
+
                // This could possibly use the parser cache if it checked the revision ID,
                // but that's more complicated than it's worth.
-               $output = $content->getParserOutput( $page->getTitle(), $rev->getId(), $options );
+               $output = $renderer->getRenderedRevision( $rev->getRevisionRecord(), $options )
+                       ->getRevisionParserOutput();
 
                // array keys will cast numeric category names to ints
                // so we need to cast them back to strings to avoid breaking things!
index e90334f..464bb60 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  */
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionRecord;
 
 /**
  * Class for viewing MediaWiki article and history.
@@ -35,11 +37,11 @@ use MediaWiki\MediaWikiServices;
 class Article implements Page {
        /**
         * @var IContextSource|null The context this Article is executed in.
-        * If null, REquestContext::getMain() is used.
+        * If null, RequestContext::getMain() is used.
         */
        protected $mContext;
 
-       /** @var WikiPage The WikiPage object of this instance */
+       /** @var WikiPage|null The WikiPage object of this instance */
        protected $mPage;
 
        /**
@@ -49,22 +51,28 @@ class Article implements Page {
        public $mParserOptions;
 
        /**
-        * @var string|null Text of the revision we are working on
-        * @todo BC cruft
-        */
-       public $mContent;
-
-       /**
-        * @var Content|null Content of the revision we are working on.
-        * Initialized by fetchContentObject().
+        * @var Content|null Content of the main slot of $this->mRevision.
+        * @note This variable is read only, setting it has no effect.
+        *       Extensions that wish to override the output of Article::view should use a hook.
+        * @todo MCR: Remove in 1.33
+        * @deprecated since 1.32
         * @since 1.21
         */
        public $mContentObject;
 
-       /** @var bool Is the content ($mContent) already loaded? */
+       /**
+        * @var bool Is the target revision loaded? Set by fetchRevisionRecord().
+        *
+        * @deprecated since 1.32. Whether content has been loaded should not be relevant to
+        * code outside this class.
+        */
        public $mContentLoaded = false;
 
-       /** @var int|null The oldid of the article that is to be shown, 0 for the current revision */
+       /**
+        * @var int|null The oldid of the article that was requested to be shown,
+        * 0 for the current revision.
+        * @see $mRevIdFetched
+        */
        public $mOldId;
 
        /** @var Title|null Title from which we were redirected here, if any. */
@@ -73,20 +81,38 @@ class Article implements Page {
        /** @var string|bool URL to redirect to or false if none */
        public $mRedirectUrl = false;
 
-       /** @var int Revision ID of revision we are working on */
+       /**
+        * @var int Revision ID of revision that was loaded.
+        * @see $mOldId
+        * @deprecated since 1.32, use getRevIdFetched() instead.
+        */
        public $mRevIdFetched = 0;
 
        /**
-        * @var Revision|null Revision we are working on. Initialized by getOldIDFromRequest()
-        * or fetchContentObject().
+        * @var Status|null represents the outcome of fetchRevisionRecord().
+        * $fetchResult->value is the RevisionRecord object, if the operation was successful.
+        *
+        * The information in $fetchResult is duplicated by the following deprecated public fields:
+        * $mRevIdFetched, $mContentLoaded. $mRevision (and $mContentObject) also typically duplicate
+        * information of the loaded revision, but may be overwritten by extensions or due to errors.
+        */
+       private $fetchResult = null;
+
+       /**
+        * @var Revision|null Revision to be shown. Initialized by getOldIDFromRequest()
+        * or fetchContentObject(). Normally loaded from the database, but may be replaced
+        * by an extension, or be a fake representing an error message or some such.
+        * While the output of Article::view is typically based on this revision,
+        * it may be overwritten by error messages or replaced by extensions.
         */
        public $mRevision = null;
 
        /**
         * @var ParserOutput|null|false The ParserOutput generated for viewing the page,
         * initialized by view(). If no ParserOutput could be generated, this is set to false.
+        * @deprecated since 1.32
         */
-       public $mParserOutput;
+       public $mParserOutput = null;
 
        /**
         * @var bool Whether render() was called. With the way subclasses work
@@ -132,7 +158,7 @@ class Article implements Page {
         */
        public static function newFromTitle( $title, IContextSource $context ) {
                if ( NS_MEDIA == $title->getNamespace() ) {
-                       // FIXME: where should this go?
+                       // XXX: This should not be here, but where should it go?
                        $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
                }
 
@@ -214,6 +240,11 @@ class Article implements Page {
                $this->mRedirectedFrom = null; # Title object if set
                $this->mRevIdFetched = 0;
                $this->mRedirectUrl = false;
+               $this->mRevision = null;
+               $this->mContentObject = null;
+               $this->fetchResult = null;
+
+               // TODO hard-deprecate direct access to public fields
 
                $this->mPage->clear();
        }
@@ -229,25 +260,15 @@ class Article implements Page {
         * This function has side effects! Do not use this function if you
         * only want the real revision text if any.
         *
-        * @return Content Return the content of this revision
+        * @deprecated since 1.32, use getRevisionFetched() or fetchRevisionRecord() instead.
+        *
+        * @return Content
         *
         * @since 1.21
         */
        protected function getContentObject() {
                if ( $this->mPage->getId() === 0 ) {
-                       # If this is a MediaWiki:x message, then load the messages
-                       # and return the message value for x.
-                       if ( $this->getTitle()->getNamespace() == NS_MEDIAWIKI ) {
-                               $text = $this->getTitle()->getDefaultMessageText();
-                               if ( $text === false ) {
-                                       $text = '';
-                               }
-
-                               $content = ContentHandler::makeContent( $text, $this->getTitle() );
-                       } else {
-                               $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
-                               $content = new MessageContent( $message, null, 'parsemag' );
-                       }
+                       $content = $this->getSubstituteContent();
                } else {
                        $this->fetchContentObject();
                        $content = $this->mContentObject;
@@ -257,7 +278,49 @@ class Article implements Page {
        }
 
        /**
-        * @return int The oldid of the article that is to be shown, 0 for the current revision
+        * Returns Content object to use when the page does not exist.
+        *
+        * @return Content
+        */
+       private function getSubstituteContent() {
+               # If this is a MediaWiki:x message, then load the messages
+               # and return the message value for x.
+               if ( $this->getTitle()->getNamespace() == NS_MEDIAWIKI ) {
+                       $text = $this->getTitle()->getDefaultMessageText();
+                       if ( $text === false ) {
+                               $text = '';
+                       }
+
+                       $content = ContentHandler::makeContent( $text, $this->getTitle() );
+               } else {
+                       $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
+                       $content = new MessageContent( $message, null, 'parsemag' );
+               }
+
+               return $content;
+       }
+
+       /**
+        * Returns ParserOutput to use when a page does not exist. In some cases, we still want to show
+        * "virtual" content, e.g. in the MediaWiki namespace, or in the File namespace for non-local
+        * files.
+        *
+        * @param ParserOptions $options
+        *
+        * @return ParserOutput
+        */
+       protected function getEmptyPageParserOutput( ParserOptions $options ) {
+               $content = $this->getSubstituteContent();
+
+               return $content->getParserOutput( $this->getTitle(), 0, $options );
+       }
+
+       /**
+        * @see getOldIDFromRequest()
+        * @see getRevIdFetched()
+        *
+        * @return int The oldid of the article that is was requested in the constructor or via the
+        *         context's WebRequest.
         */
        public function getOldID() {
                if ( is_null( $this->mOldId ) ) {
@@ -315,6 +378,8 @@ class Article implements Page {
                        }
                }
 
+               $this->mRevIdFetched = $this->mRevision ? $this->mRevision->getId() : 0;
+
                return $oldid;
        }
 
@@ -322,6 +387,7 @@ class Article implements Page {
         * Get text content object
         * Does *NOT* follow redirects.
         * @todo When is this null?
+        * @deprecated since 1.32, use fetchRevisionRecord() instead.
         *
         * @note Code that wants to retrieve page content from the database should
         * use WikiPage::getContent().
@@ -331,74 +397,139 @@ class Article implements Page {
         * @since 1.21
         */
        protected function fetchContentObject() {
-               if ( $this->mContentLoaded ) {
-                       return $this->mContentObject;
+               if ( !$this->mContentLoaded ) {
+                       $this->fetchRevisionRecord();
+               }
+
+               return $this->mContentObject;
+       }
+
+       /**
+        * Fetches the revision to work on.
+        * The revision is typically loaded from the database, but may also be a fake representing
+        * an error message or content supplied by an extension. Refer to $this->fetchResult for
+        * the revision actually loaded from the database, and any errors encountered while doing
+        * that.
+        *
+        * @return RevisionRecord|null
+        */
+       protected function fetchRevisionRecord() {
+               if ( $this->fetchResult ) {
+                       return $this->mRevision ? $this->mRevision->getRevisionRecord() : null;
                }
 
                $this->mContentLoaded = true;
-               $this->mContent = null;
+               $this->mContentObject = null;
 
                $oldid = $this->getOldID();
 
-               # Pre-fill content with error message so that if something
-               # fails we'll have something telling us what we intended.
-               // XXX: this isn't page content but a UI message. horrible.
-               $this->mContentObject = new MessageContent( 'missing-revision', [ $oldid ] );
+               // $this->mRevision might already be fetched by getOldIDFromRequest()
+               if ( !$this->mRevision ) {
+                       if ( !$oldid ) {
+                               $this->mRevision = $this->mPage->getRevision();
+
+                               if ( !$this->mRevision ) {
+                                       wfDebug( __METHOD__ . " failed to find page data for title " .
+                                               $this->getTitle()->getPrefixedText() . "\n" );
 
-               if ( $oldid ) {
-                       # $this->mRevision might already be fetched by getOldIDFromRequest()
-                       if ( !$this->mRevision ) {
+                                       // Just for sanity, output for this case is done by showMissingArticle().
+                                       $this->fetchResult = Status::newFatal( 'noarticletext' );
+                                       $this->applyContentOverride( $this->makeFetchErrorContent() );
+                                       return null;
+                               }
+                       } else {
                                $this->mRevision = Revision::newFromId( $oldid );
+
                                if ( !$this->mRevision ) {
-                                       wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" );
-                                       return false;
+                                       wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid\n" );
+
+                                       $this->fetchResult = Status::newFatal( 'missing-revision', $oldid );
+                                       $this->applyContentOverride( $this->makeFetchErrorContent() );
+                                       return null;
                                }
                        }
-               } else {
-                       $oldid = $this->mPage->getLatest();
-                       if ( !$oldid ) {
-                               wfDebug( __METHOD__ . " failed to find page data for title " .
-                                       $this->getTitle()->getPrefixedText() . "\n" );
-                               return false;
-                       }
+               }
+
+               $this->mRevIdFetched = $this->mRevision->getId();
+               $this->fetchResult = Status::newGood( $this->mRevision );
+
+               if ( !$this->mRevision->userCan( Revision::DELETED_TEXT, $this->getContext()->getUser() ) ) {
+                       wfDebug( __METHOD__ . " failed to retrieve content of revision " .
+                               $this->mRevision->getId() . "\n" );
+
+                       // Just for sanity, output for this case is done by showDeletedRevisionHeader().
+                       $this->fetchResult = Status::newFatal( 'rev-deleted-text-permission' );
+                       $this->applyContentOverride( $this->makeFetchErrorContent() );
+                       return null;
+               }
+
+               if ( Hooks::isRegistered( 'ArticleAfterFetchContentObject' ) ) {
+                       $contentObject = $this->mRevision->getContent(
+                               Revision::FOR_THIS_USER,
+                               $this->getContext()->getUser()
+                       );
 
-                       # Update error message with correct oldid
-                       $this->mContentObject = new MessageContent( 'missing-revision', [ $oldid ] );
+                       $hookContentObject = $contentObject;
 
-                       $this->mRevision = $this->mPage->getRevision();
+                               // Avoid PHP 7.1 warning of passing $this by reference
+                       $articlePage = $this;
+
+                       Hooks::run(
+                               'ArticleAfterFetchContentObject',
+                               [ &$articlePage, &$hookContentObject ],
+                               '1.32'
+                       );
 
-                       if ( !$this->mRevision ) {
-                               wfDebug( __METHOD__ . " failed to retrieve current page, rev_id $oldid\n" );
-                               return false;
+                       if ( $hookContentObject !== $contentObject ) {
+                               // A hook handler is trying to override the content
+                               $this->applyContentOverride( $hookContentObject );
                        }
                }
 
-               // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
-               // We should instead work with the Revision object when we need it...
-               // Loads if user is allowed
-               $content = $this->mRevision->getContent(
+               // For B/C only
+               $this->mContentObject = $this->mRevision->getContent(
                        Revision::FOR_THIS_USER,
                        $this->getContext()->getUser()
                );
 
-               if ( !$content ) {
-                       wfDebug( __METHOD__ . " failed to retrieve content of revision " .
-                               $this->mRevision->getId() . "\n" );
-                       return false;
+               return $this->mRevision->getRevisionRecord();
+       }
+
+       /**
+        * Returns a Content object representing any error in $this->fetchContent, or null
+        * if there is no such error.
+        *
+        * @return Content|null
+        */
+       private function makeFetchErrorContent() {
+               if ( !$this->fetchResult || $this->fetchResult->isOK() ) {
+                       return null;
                }
 
-               $this->mContentObject = $content;
-               $this->mRevIdFetched = $this->mRevision->getId();
+               return new MessageContent( $this->fetchResult->getMessage() );
+       }
 
-               // Avoid PHP 7.1 warning of passing $this by reference
-               $articlePage = $this;
+       /**
+        * Applies a content override by constructing a fake Revision object and assigning
+        * it to mRevision. The fake revision will not have a user, timestamp or summary set.
+        *
+        * This mechanism exists mainly to accommodate extensions that use the
+        * ArticleAfterFetchContentObject. Once that hook has been removed, there should no longer
+        * be a need for a fake revision object. fetchRevisionRecord() presently also uses this mechanism
+        * to report errors, but that could be changed to use $this->fetchResult instead.
+        *
+        * @param Content $override Content to be used instead of the actual page content,
+        *        coming from an extension or representing an error message.
+        */
+       private function applyContentOverride( Content $override ) {
+               // Construct a fake revision
+               $rev = new MutableRevisionRecord( $this->getTitle() );
+               $rev->setContent( 'main', $override );
 
-               Hooks::run(
-                       'ArticleAfterFetchContentObject',
-                       [ &$articlePage, &$this->mContentObject ]
-               );
+               $this->mRevision = new Revision( $rev );
 
-               return $this->mContentObject;
+               // For B/C only
+               $this->mContentObject = $override;
        }
 
        /**
@@ -417,25 +548,32 @@ class Article implements Page {
 
        /**
         * Get the fetched Revision object depending on request parameters or null
-        * on failure.
+        * on failure. The revision returned may be a fake representing an error message or
+        * wrapping content supplied by an extension. Refer to $this->fetchResult for the
+        * revision actually loaded from the database.
         *
         * @since 1.19
         * @return Revision|null
         */
        public function getRevisionFetched() {
-               $this->fetchContentObject();
+               $this->fetchRevisionRecord();
 
-               return $this->mRevision;
+               if ( $this->fetchResult->isOK() ) {
+                       return $this->mRevision;
+               }
        }
 
        /**
         * Use this to fetch the rev ID used on page views
         *
+        * Before fetchRevisionRecord was called, this returns the page's latest revision,
+        * regardless of what getOldID() returns.
+        *
         * @return int Revision ID of last article revision
         */
        public function getRevIdFetched() {
-               if ( $this->mRevIdFetched ) {
-                       return $this->mRevIdFetched;
+               if ( $this->fetchResult && $this->fetchResult->isOK() ) {
+                       return $this->fetchResult->value->getId();
                } else {
                        return $this->mPage->getLatest();
                }
@@ -571,11 +709,9 @@ class Article implements Page {
                                        }
                                        break;
                                case 3:
-                                       # This will set $this->mRevision if needed
-                                       $this->fetchContentObject();
-
                                        # Are we looking at an old revision
-                                       if ( $oldid && $this->mRevision ) {
+                                       $rev = $this->fetchRevisionRecord();
+                                       if ( $oldid && $this->fetchResult->isOK() ) {
                                                $this->setOldSubtitle( $oldid );
 
                                                if ( !$this->showDeletedRevisionHeader() ) {
@@ -599,10 +735,21 @@ class Article implements Page {
                                                        "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
                                                        'clearyourcache'
                                                );
+                                       } elseif ( !Hooks::run( 'ArticleRevisionViewCustom', [
+                                                       $rev,
+                                                       $this->getTitle(),
+                                                       $oldid,
+                                                       $outputPage,
+                                               ] )
+                                       ) {
+                                               // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
+                                               // Allow extensions do their own custom view for certain pages
+                                               $outputDone = true;
                                        } elseif ( !Hooks::run( 'ArticleContentViewCustom',
-                                               [ $this->fetchContentObject(), $this->getTitle(), $outputPage ] )
+                                               [ $this->fetchContentObject(), $this->getTitle(), $outputPage ], '1.32' )
                                        ) {
-                                               # Allow extensions do their own custom view for certain pages
+                                               // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
+                                               // Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        }
                                        break;
@@ -610,12 +757,32 @@ class Article implements Page {
                                        # Run the parse, protected by a pool counter
                                        wfDebug( __METHOD__ . ": doing uncached parse\n" );
 
-                                       $content = $this->getContentObject();
-                                       $poolArticleView = new PoolWorkArticleView( $this->getPage(), $parserOptions,
-                                               $this->getRevIdFetched(), $useParserCache, $content );
+                                       $rev = $this->fetchRevisionRecord();
+                                       $error = null;
 
-                                       if ( !$poolArticleView->execute() ) {
+                                       if ( $rev ) {
+                                               $poolArticleView = new PoolWorkArticleView(
+                                                       $this->getPage(),
+                                                       $parserOptions,
+                                                       $this->getRevIdFetched(),
+                                                       $useParserCache,
+                                                       $rev
+                                               );
+                                               $ok = $poolArticleView->execute();
                                                $error = $poolArticleView->getError();
+                                               $this->mParserOutput = $poolArticleView->getParserOutput() ?: null;
+
+                                               # Don't cache a dirty ParserOutput object
+                                               if ( $poolArticleView->getIsDirty() ) {
+                                                       $outputPage->setCdnMaxage( 0 );
+                                                       $outputPage->addHTML( "<!-- parser cache is expired, " .
+                                                               "sending anyway due to pool overload-->\n" );
+                                               }
+                                       } else {
+                                               $ok = false;
+                                       }
+
+                                       if ( !$ok ) {
                                                if ( $error ) {
                                                        $outputPage->clearHTML(); // for release() errors
                                                        $outputPage->enableClientCache( false );
@@ -628,18 +795,13 @@ class Article implements Page {
                                                return;
                                        }
 
-                                       $this->mParserOutput = $poolArticleView->getParserOutput();
-                                       $outputPage->addParserOutput( $this->mParserOutput, $poOptions );
-                                       if ( $content->getRedirectTarget() ) {
-                                               $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
-                                                       $this->getContext()->msg( 'redirectpagesub' )->parse() . "</span>" );
+                                       if ( $this->mParserOutput ) {
+                                               $outputPage->addParserOutput( $this->mParserOutput, $poOptions );
                                        }
 
-                                       # Don't cache a dirty ParserOutput object
-                                       if ( $poolArticleView->getIsDirty() ) {
-                                               $outputPage->setCdnMaxage( 0 );
-                                               $outputPage->addHTML( "<!-- parser cache is expired, " .
-                                                       "sending anyway due to pool overload-->\n" );
+                                       if ( $rev && $this->getRevisionRedirectTarget( $rev ) ) {
+                                               $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
+                                                       $this->getContext()->msg( 'redirectpagesub' )->parse() . "</span>" );
                                        }
 
                                        $outputDone = true;
@@ -650,8 +812,10 @@ class Article implements Page {
                        }
                }
 
-               # Get the ParserOutput actually *displayed* here.
-               # Note that $this->mParserOutput is the *current*/oldid version output.
+               // Get the ParserOutput actually *displayed* here.
+               // Note that $this->mParserOutput is the *current*/oldid version output.
+               // Note that the ArticleViewHeader hook is allowed to set $outputDone to a
+               // ParserOutput instance.
                $pOutput = ( $outputDone instanceof ParserOutput )
                        ? $outputDone // object fetched by hook
                        : $this->mParserOutput ?: null; // ParserOutput or null, avoid false
@@ -677,12 +841,12 @@ class Article implements Page {
                $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), IExpiringStore::TTL_DAY );
 
                # Check for any __NOINDEX__ tags on the page using $pOutput
-               $policy = $this->getRobotPolicy( 'view', $pOutput );
+               $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
                $outputPage->setIndexPolicy( $policy['index'] );
-               $outputPage->setFollowPolicy( $policy['follow'] );
+               $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
 
                $this->showViewFooter();
-               $this->mPage->doViewUpdates( $user, $oldid );
+               $this->mPage->doViewUpdates( $user, $oldid ); // FIXME: test this
 
                # Load the postEdit module if the user just saved this revision
                # See also EditPage::setPostEditCookie
@@ -693,10 +857,22 @@ class Article implements Page {
                        # Clear the cookie. This also prevents caching of the response.
                        $request->response()->clearCookie( $cookieKey );
                        $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
-                       $outputPage->addModules( 'mediawiki.action.view.postEdit' );
+                       $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
                }
        }
 
+       /**
+        * @param RevisionRecord $revision
+        * @return null|Title
+        */
+       private function getRevisionRedirectTarget( RevisionRecord $revision ) {
+               // TODO: find a *good* place for the code that determines the redirect target for
+               // a given revision!
+               // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
+               $content = $revision->getContent( 'main' );
+               return $content ? $content->getRedirectTarget() : null;
+       }
+
        /**
         * Adjust title for pages with displaytitle, -{T|}- or language conversion
         * @param ParserOutput $pOutput
@@ -1261,7 +1437,9 @@ class Article implements Page {
                # Show error message
                $oldid = $this->getOldID();
                if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
-                       $outputPage->addParserOutput( $this->getContentObject()->getParserOutput( $title ) );
+                       // use fake Content object for system message
+                       $parserOptions = ParserOptions::newCanonical( 'canonical' );
+                       $outputPage->addParserOutput( $this->getEmptyPageParserOutput( $parserOptions ) );
                } else {
                        if ( $oldid ) {
                                $text = wfMessage( 'missing-revision', $oldid )->plain();
@@ -1670,7 +1848,7 @@ class Article implements Page {
                                __METHOD__
                        );
 
-                       // @todo FIXME: i18n issue/patchwork message
+                       // @todo i18n issue/patchwork message
                        $context->getOutput()->addHTML(
                                '<strong class="mw-delete-warning-revisions">' .
                                $context->msg( 'historywarning' )->numParams( $revisions )->parse() .
@@ -1697,7 +1875,7 @@ class Article implements Page {
 
        /**
         * Output deletion confirmation dialog
-        * @todo FIXME: Move to another file?
+        * @todo Move to another file?
         * @param string $reason Prefilled reason
         */
        public function confirmDelete( $reason ) {
index 58f25d4..d3d6da7 100644 (file)
@@ -272,18 +272,20 @@ class ImagePage extends Article {
        }
 
        /**
-        * Overloading Article's getContentObject method.
+        * Overloading Article's getEmptyPageParserOutput method.
         *
         * Omit noarticletext if sharedupload; text will be fetched from the
         * shared upload server if possible.
-        * @return string
+        *
+        * @param ParserOptions $options
+        * @return ParserOutput
         */
-       public function getContentObject() {
+       public function getEmptyPageParserOutput( ParserOptions $options ) {
                $this->loadFile();
                if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getId() ) {
-                       return null;
+                       return new ParserOutput();
                }
-               return parent::getContentObject();
+               return parent::getEmptyPageParserOutput( $options );
        }
 
        private function getLanguageForRendering( WebRequest $request, File $file ) {
index 4af86ae..286494e 100644 (file)
  *
  * @file
  */
+
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRenderer;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
 
 class PoolWorkArticleView extends PoolCounterWork {
        /** @var WikiPage */
@@ -35,8 +40,14 @@ class PoolWorkArticleView extends PoolCounterWork {
        /** @var ParserOptions */
        private $parserOptions;
 
-       /** @var Content|null */
-       private $content = null;
+       /** @var RevisionRecord|null */
+       private $revision = null;
+
+       /** @var RevisionStore */
+       private $revisionStore = null;
+
+       /** @var RevisionRenderer */
+       private $renderer = null;
 
        /** @var ParserOutput|bool */
        private $parserOutput = false;
@@ -53,26 +64,52 @@ class PoolWorkArticleView extends PoolCounterWork {
         * @param int $revid ID of the revision being parsed.
         * @param bool $useParserCache Whether to use the parser cache.
         *   operation.
-        * @param Content|string|null $content Content to parse or null to load it; may
-        *   also be given as a wikitext string, for BC.
+        * @param RevisionRecord|Content|string|null $revision Revision to render, or null to load it;
+        *        may also be given as a wikitext string, or a Content object, for BC.
         */
        public function __construct( WikiPage $page, ParserOptions $parserOptions,
-               $revid, $useParserCache, $content = null
+               $revid, $useParserCache, $revision = null
        ) {
-               if ( is_string( $content ) ) { // BC: old style call
+               if ( is_string( $revision ) ) { // BC: very old style call
                        $modelId = $page->getRevision()->getContentModel();
                        $format = $page->getRevision()->getContentFormat();
-                       $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format );
+                       $revision = ContentHandler::makeContent( $revision, $page->getTitle(), $modelId, $format );
+               }
+
+               if ( $revision instanceof Content ) { // BC: old style call
+                       $content = $revision;
+                       $revision = new MutableRevisionRecord( $page->getTitle() );
+                       $revision->setId( $revid );
+                       $revision->setPageId( $page->getId() );
+                       $revision->setContent( 'main', $content );
                }
 
+               if ( $revision ) {
+                       // Check that the RevisionRecord matches $revid and $page, but still allow
+                       // fake RevisionRecords coming from errors or hooks in Article to be rendered.
+                       if ( $revision->getId() && $revision->getId() !== $revid ) {
+                               throw new InvalidArgumentException( '$revid parameter mismatches $revision parameter' );
+                       }
+                       if ( $revision->getPageId()
+                               && $revision->getPageId() !== $page->getTitle()->getArticleID()
+                       ) {
+                               throw new InvalidArgumentException( '$page parameter mismatches $revision parameter' );
+                       }
+               }
+
+               // TODO: DI: inject services
+               $this->renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
+               $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+               $this->parserCache = MediaWikiServices::getInstance()->getParserCache();
+
                $this->page = $page;
                $this->revid = $revid;
                $this->cacheable = $useParserCache;
                $this->parserOptions = $parserOptions;
-               $this->content = $content;
-               $this->parserCache = MediaWikiServices::getInstance()->getParserCache();
+               $this->revision = $revision;
                $this->cacheKey = $this->parserCache->getKey( $page, $parserOptions );
                $keyPrefix = $this->cacheKey ?: wfMemcKey( 'articleview', 'missingcachekey' );
+
                parent::__construct( 'ArticleView', $keyPrefix . ':revid:' . $revid );
        }
 
@@ -114,23 +151,33 @@ class PoolWorkArticleView extends PoolCounterWork {
 
                $isCurrent = $this->revid === $this->page->getLatest();
 
-               if ( $this->content !== null ) {
-                       $content = $this->content;
+               // Bypass audience check for current revision
+               $audience = $isCurrent ? RevisionRecord::RAW : RevisionRecord::FOR_PUBLIC;
+
+               if ( $this->revision !== null ) {
+                       $rev = $this->revision;
                } elseif ( $isCurrent ) {
-                       // XXX: why use RAW audience here, and PUBLIC (default) below?
-                       $content = $this->page->getContent( Revision::RAW );
+                       $rev = $this->page->getRevision()
+                               ? $this->page->getRevision()->getRevisionRecord()
+                               : null;
                } else {
-                       $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
+                       $rev = $this->revisionStore->getRevisionByTitle( $this->page->getTitle(), $this->revid );
+               }
 
-                       if ( $rev === null ) {
-                               $content = null;
-                       } else {
-                               // XXX: why use PUBLIC audience here (default), and RAW above?
-                               $content = $rev->getContent();
-                       }
+               if ( !$rev ) {
+                       // couldn't load
+                       return false;
                }
 
-               if ( $content === null ) {
+               $renderedRevision = $this->renderer->getRenderedRevision(
+                       $rev,
+                       $this->parserOptions,
+                       null,
+                       [ 'audience' => $audience ]
+               );
+
+               if ( !$renderedRevision ) {
+                       // audience check failed
                        return false;
                }
 
@@ -138,11 +185,7 @@ class PoolWorkArticleView extends PoolCounterWork {
                $cacheTime = wfTimestampNow();
 
                $time = - microtime( true );
-               $this->parserOutput = $content->getParserOutput(
-                       $this->page->getTitle(),
-                       $this->revid,
-                       $this->parserOptions
-               );
+               $this->parserOutput = $renderedRevision->getRevisionParserOutput();
                $time += microtime( true );
 
                // Timing hack
index 3069bd8..a929820 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\RevisionRecord;
 use Wikimedia\Rdbms\IResultWrapper;
 
 /**
@@ -421,8 +423,9 @@ class SpecialUndelete extends SpecialPage {
                $t = $lang->userTime( $timestamp, $user );
                $userLink = Linker::revUserTools( $rev );
 
-               $content = $rev->getContent( Revision::FOR_THIS_USER, $user );
+               $content = $rev->getContent( RevisionRecord::FOR_THIS_USER, $user );
 
+               // TODO: MCR: this will have to become something like $hasTextSlots and $hasNonTextSlots
                $isText = ( $content instanceof TextContent );
 
                if ( $this->mPreview || $isText ) {
@@ -447,12 +450,23 @@ class SpecialUndelete extends SpecialPage {
                        return;
                }
 
-               if ( ( $this->mPreview || !$isText ) && $content ) {
+               if ( $this->mPreview || !$isText ) {
                        // NOTE: non-text content has no source view, so always use rendered preview
 
                        $popts = $out->parserOptions();
+                       $renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
+
+                       $rendered = $renderer->getRenderedRevision(
+                               $rev->getRevisionRecord(),
+                               $popts,
+                               $user,
+                               [ 'audience' => RevisionRecord::FOR_THIS_USER ]
+                       );
+
+                       // Fail hard if the audience check fails, since we already checked
+                       // at the beginning of this method.
+                       $pout = $rendered->getRevisionParserOutput();
 
-                       $pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
                        $out->addParserOutput( $pout, [
                                'enableSectionEditLinks' => false,
                        ] );
@@ -462,6 +476,7 @@ class SpecialUndelete extends SpecialPage {
                $buttonFields = [];
 
                if ( $isText ) {
+                       // TODO: MCR: make this work for multiple slots
                        // source view for textual content
                        $sourceView = Xml::element( 'textarea', [
                                'readonly' => 'readonly',
index b12974b..2cafc1b 100644 (file)
@@ -44,6 +44,7 @@ class CompareParserCache extends Maintenance {
                $withcache = 0;
                $withdiff = 0;
                $parserCache = MediaWikiServices::getInstance()->getParserCache();
+               $renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
                while ( $pages-- > 0 ) {
                        $row = $dbr->selectRow( 'page',
                                // @todo Title::selectFields() or Title::getQueryInfo() or something
@@ -69,17 +70,16 @@ class CompareParserCache extends Maintenance {
 
                        $title = Title::newFromRow( $row );
                        $page = WikiPage::factory( $title );
-                       $revision = $page->getRevision();
-                       $content = $revision->getContent( Revision::RAW );
-
+                       $revision = $page->getRevision()->getRevisionRecord();
                        $parserOptions = $page->makeParserOptions( 'canonical' );
 
                        $parserOutputOld = $parserCache->get( $page, $parserOptions );
 
                        if ( $parserOutputOld ) {
                                $t1 = microtime( true );
-                               $parserOutputNew = $content->getParserOutput(
-                                       $title, $revision->getId(), $parserOptions, false );
+                               $parserOutputNew = $renderer->getRenderedRevision( $revision, $parserOptions )
+                                       ->getRevisionParserOutput();
+
                                $sec = microtime( true ) - $t1;
                                $totalsec += $sec;
 
index a2a9d09..bea0b49 100644 (file)
@@ -6,7 +6,11 @@ use Content;
 use Language;
 use MediaWiki\Revision\RenderedRevision;
 use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\MutableRevisionSlots;
+use MediaWiki\Storage\RevisionArchiveRecord;
 use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\RevisionStoreRecord;
 use MediaWiki\Storage\SuppressedDataException;
 use MediaWiki\User\UserIdentityValue;
 use MediaWikiTestCase;
@@ -15,6 +19,7 @@ use ParserOutput;
 use PHPUnit\Framework\MockObject\MockObject;
 use Title;
 use User;
+use Wikimedia\TestingAccessWrapper;
 use WikitextContent;
 
 /**
@@ -86,13 +91,13 @@ class RenderedRevisionTest extends MediaWikiTestCase {
                        ->will( $this->returnValue( NS_MAIN ) );
                $mock->expects( $this->any() )
                        ->method( 'getText' )
-                       ->will( $this->returnValue( __CLASS__ ) );
+                       ->will( $this->returnValue( 'RenderTestPage' ) );
                $mock->expects( $this->any() )
                        ->method( 'getPrefixedText' )
-                       ->will( $this->returnValue( __CLASS__ ) );
+                       ->will( $this->returnValue( 'RenderTestPage' ) );
                $mock->expects( $this->any() )
                        ->method( 'getDBkey' )
-                       ->will( $this->returnValue( __CLASS__ ) );
+                       ->will( $this->returnValue( 'RenderTestPage' ) );
                $mock->expects( $this->any() )
                        ->method( 'getArticleID' )
                        ->will( $this->returnValue( $articleId ) );
@@ -111,7 +116,7 @@ class RenderedRevisionTest extends MediaWikiTestCase {
                $mock->expects( $this->any() )
                        ->method( 'equals' )
                        ->willReturnCallback( function ( Title $other ) use ( $mock ) {
-                               return $mock->getArticleID() === $other->getArticleID();
+                               return $mock->getPrefixedText() === $other->getPrefixedText();
                        } );
                $mock->expects( $this->any() )
                        ->method( 'userCan' )
@@ -122,21 +127,66 @@ class RenderedRevisionTest extends MediaWikiTestCase {
                return $mock;
        }
 
-       public function testGetRevisionParserOutput_new() {
-               $title = $this->getMockTitle( 7, 21 );
+       /**
+        * @param string $class
+        * @param Title $title
+        * @param null|int $id
+        * @param int $visibility
+        * @return RevisionRecord
+        */
+       private function getMockRevision(
+               $class,
+               $title,
+               $id = null,
+               $visibility = 0,
+               array $content = null
+       ) {
+               $frank = new UserIdentityValue( 9, 'Frank', 0 );
+
+               if ( !$content ) {
+                       $text = "";
+                       $text .= "* page:{{PAGENAME}}!\n";
+                       $text .= "* rev:{{REVISIONID}}!\n";
+                       $text .= "* user:{{REVISIONUSER}}!\n";
+                       $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
+                       $text .= "* [[Link It]]\n";
+
+                       $content = [ 'main' => new WikitextContent( $text ) ];
+               }
 
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
-               $rev->setTimestamp( '20180101000003' );
+               /** @var MockObject|RevisionRecord $mock */
+               $mock = $this->getMockBuilder( $class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [
+                               'getId',
+                               'getPageId',
+                               'getPageAsLinkTarget',
+                               'getUser',
+                               'getVisibility',
+                               'getTimestamp',
+                       ] )->getMock();
+
+               $mock->method( 'getId' )->willReturn( $id );
+               $mock->method( 'getPageId' )->willReturn( $title->getArticleID() );
+               $mock->method( 'getPageAsLinkTarget' )->willReturn( $title );
+               $mock->method( 'getUser' )->willReturn( $frank );
+               $mock->method( 'getVisibility' )->willReturn( $visibility );
+               $mock->method( 'getTimestamp' )->willReturn( '20180101000003' );
+
+               /** @var object $mockAccess */
+               $mockAccess = TestingAccessWrapper::newFromObject( $mock );
+               $mockAccess->mSlots = new MutableRevisionSlots();
+
+               foreach ( $content as $role => $cnt ) {
+                       $mockAccess->mSlots->setContent( $role, $cnt );
+               }
 
-               $text = "";
-               $text .= "* page:{{PAGENAME}}\n";
-               $text .= "* rev:{{REVISIONID}}\n";
-               $text .= "* user:{{REVISIONUSER}}\n";
-               $text .= "* time:{{REVISIONTIMESTAMP}}\n";
-               $text .= "* [[Link It]]\n";
+               return $mock;
+       }
 
-               $rev->setContent( 'main', new WikitextContent( $text ) );
+       public function testGetRevisionParserOutput_new() {
+               $title = $this->getMockTitle( 0, 21 );
+               $rev = $this->getMockRevision( RevisionStoreRecord::class, $title );
 
                $options = ParserOptions::newCanonical( 'canonical' );
                $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
@@ -148,26 +198,33 @@ class RenderedRevisionTest extends MediaWikiTestCase {
 
                $html = $rr->getRevisionParserOutput()->getText();
 
-               $this->assertContains( 'page:' . __CLASS__, $html );
-               $this->assertContains( 'user:Frank', $html );
-               $this->assertContains( 'time:20180101000003', $html );
+               $this->assertContains( 'page:RenderTestPage!', $html );
+               $this->assertContains( 'user:Frank!', $html );
+               $this->assertContains( 'time:20180101000003!', $html );
        }
 
-       public function testGetRevisionParserOutput_current() {
-               $title = $this->getMockTitle( 7, 21 );
+       public function testGetRevisionParserOutput_previewWithSelfTransclusion() {
+               $title = $this->getMockTitle( 0, 21 );
+               $name = $title->getPrefixedText();
 
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setId( 21 ); // current!
-               $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
-               $rev->setTimestamp( '20180101000003' );
+               $text = "(ONE)<includeonly>(TWO)</includeonly><noinclude>#{{:$name}}#</noinclude>";
 
-               $text = "";
-               $text .= "* page:{{PAGENAME}}\n";
-               $text .= "* rev:{{REVISIONID}}\n";
-               $text .= "* user:{{REVISIONUSER}}\n";
-               $text .= "* time:{{REVISIONTIMESTAMP}}\n";
+               $content = [
+                       'main' => new WikitextContent( $text )
+               ];
 
-               $rev->setContent( 'main', new WikitextContent( $text ) );
+               $rev = $this->getMockRevision( RevisionStoreRecord::class, $title, null, 0, $content );
+
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
+
+               $html = $rr->getRevisionParserOutput()->getText();
+               $this->assertContains( '(ONE)#(ONE)(TWO)#', $html );
+       }
+
+       public function testGetRevisionParserOutput_current() {
+               $title = $this->getMockTitle( 7, 21 );
+               $rev = $this->getMockRevision( RevisionStoreRecord::class, $title, 21 );
 
                $options = ParserOptions::newCanonical( 'canonical' );
                $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
@@ -179,29 +236,39 @@ class RenderedRevisionTest extends MediaWikiTestCase {
 
                $html = $rr->getRevisionParserOutput()->getText();
 
-               $this->assertContains( 'page:' . __CLASS__, $html );
-               $this->assertContains( 'rev:21', $html );
-               $this->assertContains( 'user:Frank', $html );
-               $this->assertContains( 'time:20180101000003', $html );
+               $this->assertContains( 'page:RenderTestPage!', $html );
+               $this->assertContains( 'rev:21!', $html );
+               $this->assertContains( 'user:Frank!', $html );
+               $this->assertContains( 'time:20180101000003!', $html );
 
                $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
        }
 
        public function testGetRevisionParserOutput_old() {
                $title = $this->getMockTitle( 7, 21 );
+               $rev = $this->getMockRevision( RevisionStoreRecord::class, $title, 11 );
 
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setId( 11 ); // old!
-               $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
-               $rev->setTimestamp( '20180101000003' );
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
 
-               $text = "";
-               $text .= "* page:{{PAGENAME}}\n";
-               $text .= "* rev:{{REVISIONID}}\n";
-               $text .= "* user:{{REVISIONUSER}}\n";
-               $text .= "* time:{{REVISIONTIMESTAMP}}\n";
+               $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
 
-               $rev->setContent( 'main', new WikitextContent( $text ) );
+               $this->assertSame( $rev, $rr->getRevision() );
+               $this->assertSame( $options, $rr->getOptions() );
+
+               $html = $rr->getRevisionParserOutput()->getText();
+
+               $this->assertContains( 'page:RenderTestPage!', $html );
+               $this->assertContains( 'rev:11!', $html );
+               $this->assertContains( 'user:Frank!', $html );
+               $this->assertContains( 'time:20180101000003!', $html );
+
+               $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
+       }
+
+       public function testGetRevisionParserOutput_archive() {
+               $title = $this->getMockTitle( 7, 21 );
+               $rev = $this->getMockRevision( RevisionArchiveRecord::class, $title, 11 );
 
                $options = ParserOptions::newCanonical( 'canonical' );
                $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
@@ -213,30 +280,22 @@ class RenderedRevisionTest extends MediaWikiTestCase {
 
                $html = $rr->getRevisionParserOutput()->getText();
 
-               $this->assertContains( 'page:' . __CLASS__, $html );
-               $this->assertContains( 'rev:11', $html );
-               $this->assertContains( 'user:Frank', $html );
-               $this->assertContains( 'time:20180101000003', $html );
+               $this->assertContains( 'page:RenderTestPage!', $html );
+               $this->assertContains( 'rev:11!', $html );
+               $this->assertContains( 'user:Frank!', $html );
+               $this->assertContains( 'time:20180101000003!', $html );
 
                $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
        }
 
        public function testGetRevisionParserOutput_suppressed() {
                $title = $this->getMockTitle( 7, 21 );
-
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setId( 11 ); // old!
-               $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
-               $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
-               $rev->setTimestamp( '20180101000003' );
-
-               $text = "";
-               $text .= "* page:{{PAGENAME}}\n";
-               $text .= "* rev:{{REVISIONID}}\n";
-               $text .= "* user:{{REVISIONUSER}}\n";
-               $text .= "* time:{{REVISIONTIMESTAMP}}\n";
-
-               $rev->setContent( 'main', new WikitextContent( $text ) );
+               $rev = $this->getMockRevision(
+                       RevisionStoreRecord::class,
+                       $title,
+                       11,
+                       RevisionRecord::DELETED_TEXT
+               );
 
                $options = ParserOptions::newCanonical( 'canonical' );
                $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
@@ -247,20 +306,12 @@ class RenderedRevisionTest extends MediaWikiTestCase {
 
        public function testGetRevisionParserOutput_privileged() {
                $title = $this->getMockTitle( 7, 21 );
-
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setId( 11 ); // old!
-               $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
-               $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
-               $rev->setTimestamp( '20180101000003' );
-
-               $text = "";
-               $text .= "* page:{{PAGENAME}}\n";
-               $text .= "* rev:{{REVISIONID}}\n";
-               $text .= "* user:{{REVISIONUSER}}\n";
-               $text .= "* time:{{REVISIONTIMESTAMP}}\n";
-
-               $rev->setContent( 'main', new WikitextContent( $text ) );
+               $rev = $this->getMockRevision(
+                       RevisionStoreRecord::class,
+                       $title,
+                       11,
+                       RevisionRecord::DELETED_TEXT
+               );
 
                $options = ParserOptions::newCanonical( 'canonical' );
                $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
@@ -281,30 +332,22 @@ class RenderedRevisionTest extends MediaWikiTestCase {
                $html = $rr->getRevisionParserOutput()->getText();
 
                // Suppressed content should be visible for sysops
-               $this->assertContains( 'page:' . __CLASS__, $html );
-               $this->assertContains( 'rev:11', $html );
-               $this->assertContains( 'user:Frank', $html );
-               $this->assertContains( 'time:20180101000003', $html );
+               $this->assertContains( 'page:RenderTestPage!', $html );
+               $this->assertContains( 'rev:11!', $html );
+               $this->assertContains( 'user:Frank!', $html );
+               $this->assertContains( 'time:20180101000003!', $html );
 
                $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
        }
 
        public function testGetRevisionParserOutput_raw() {
                $title = $this->getMockTitle( 7, 21 );
-
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setId( 11 ); // old!
-               $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
-               $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
-               $rev->setTimestamp( '20180101000003' );
-
-               $text = "";
-               $text .= "* page:{{PAGENAME}}\n";
-               $text .= "* rev:{{REVISIONID}}\n";
-               $text .= "* user:{{REVISIONUSER}}\n";
-               $text .= "* time:{{REVISIONTIMESTAMP}}\n";
-
-               $rev->setContent( 'main', new WikitextContent( $text ) );
+               $rev = $this->getMockRevision(
+                       RevisionStoreRecord::class,
+                       $title,
+                       11,
+                       RevisionRecord::DELETED_TEXT
+               );
 
                $options = ParserOptions::newCanonical( 'canonical' );
                $rr = new RenderedRevision(
@@ -323,23 +366,22 @@ class RenderedRevisionTest extends MediaWikiTestCase {
                $html = $rr->getRevisionParserOutput()->getText();
 
                // Suppressed content should be visible for sysops
-               $this->assertContains( 'page:' . __CLASS__, $html );
-               $this->assertContains( 'rev:11', $html );
-               $this->assertContains( 'user:Frank', $html );
-               $this->assertContains( 'time:20180101000003', $html );
+               $this->assertContains( 'page:RenderTestPage!', $html );
+               $this->assertContains( 'rev:11!', $html );
+               $this->assertContains( 'user:Frank!', $html );
+               $this->assertContains( 'time:20180101000003!', $html );
 
                $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
        }
 
        public function testGetRevisionParserOutput_multi() {
-               $title = $this->getMockTitle( 7, 21 );
-
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
-               $rev->setTimestamp( '20180101000003' );
+               $content = [
+                       'main' => new WikitextContent( '[[Kittens]]' ),
+                       'aux' => new WikitextContent( '[[Goats]]' ),
+               ];
 
-               $rev->setContent( 'main', new WikitextContent( '[[Kittens]]' ) );
-               $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
+               $title = $this->getMockTitle( 7, 21 );
+               $rev = $this->getMockRevision( RevisionStoreRecord::class, $title, 11, 0, $content );
 
                $options = ParserOptions::newCanonical( 'canonical' );
                $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
@@ -369,6 +411,77 @@ class RenderedRevisionTest extends MediaWikiTestCase {
                $this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
        }
 
+       public function testGetRevisionParserOutput_incompleteNoId() {
+               $title = $this->getMockTitle( 7, 21 );
+
+               $rev = new MutableRevisionRecord( $title );
+
+               $text = "";
+               $text .= "* page:{{PAGENAME}}!\n";
+               $text .= "* rev:{{REVISIONID}}!\n";
+               $text .= "* user:{{REVISIONUSER}}!\n";
+               $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
+
+               $rev->setContent( 'main', new WikitextContent( $text ) );
+
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
+
+               // MutableRevisionRecord without ID should be used by the parser.
+               // USeful for fake
+               $html = $rr->getRevisionParserOutput()->getText();
+
+               $this->assertContains( 'page:RenderTestPage!', $html );
+               $this->assertContains( 'rev:!', $html );
+               $this->assertContains( 'user:!', $html );
+               $this->assertContains( 'time:!', $html );
+       }
+
+       public function testGetRevisionParserOutput_incompleteWithId() {
+               $title = $this->getMockTitle( 7, 21 );
+
+               $rev = new MutableRevisionRecord( $title );
+               $rev->setId( 21 );
+
+               $text = "";
+               $text .= "* page:{{PAGENAME}}!\n";
+               $text .= "* rev:{{REVISIONID}}!\n";
+               $text .= "* user:{{REVISIONUSER}}!\n";
+               $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
+
+               $rev->setContent( 'main', new WikitextContent( $text ) );
+
+               $actualRevision = $this->getMockRevision(
+                       RevisionStoreRecord::class,
+                       $title,
+                       21,
+                       RevisionRecord::DELETED_TEXT
+               );
+
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
+
+               // MutableRevisionRecord with ID should not be used by the parser,
+               // revision should be loaded instead!
+               $revisionStore = $this->getMockBuilder( RevisionStore::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $revisionStore->expects( $this->once() )
+                       ->method( 'getKnownCurrentRevision' )
+                       ->with( $title, 0 )
+                       ->willReturn( $actualRevision );
+
+               $this->setService( 'RevisionStore', $revisionStore );
+
+               $html = $rr->getRevisionParserOutput()->getText();
+
+               $this->assertContains( 'page:RenderTestPage!', $html );
+               $this->assertContains( 'rev:21!', $html );
+               $this->assertContains( 'user:Frank!', $html );
+               $this->assertContains( 'time:20180101000003!', $html );
+       }
+
        public function testNoHtml() {
                /** @var MockObject|Content $mockContent */
                $mockContent = $this->getMockBuilder( WikitextContent::class )
@@ -409,10 +522,10 @@ class RenderedRevisionTest extends MediaWikiTestCase {
                $rev = new MutableRevisionRecord( $title );
 
                $text = "";
-               $text .= "* page:{{PAGENAME}}\n";
-               $text .= "* rev:{{REVISIONID}}\n";
-               $text .= "* user:{{REVISIONUSER}}\n";
-               $text .= "* time:{{REVISIONTIMESTAMP}}\n";
+               $text .= "* page:{{PAGENAME}}!\n";
+               $text .= "* rev:{{REVISIONID}}!\n";
+               $text .= "* user:{{REVISIONUSER}}!\n";
+               $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
 
                $rev->setContent( 'main', new WikitextContent( $text ) );
                $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
@@ -441,10 +554,10 @@ class RenderedRevisionTest extends MediaWikiTestCase {
                $html = $updatedOutput->getText();
 
                $this->assertNotSame( $firstOutput, $updatedOutput, 'Reset merged' );
-               $this->assertContains( 'page:' . __CLASS__, $html );
-               $this->assertContains( 'rev:23', $html );
-               $this->assertContains( 'user:Frank', $html );
-               $this->assertContains( 'time:20180101000003', $html );
+               $this->assertContains( 'page:RenderTestPage!', $html );
+               $this->assertContains( 'rev:23!', $html );
+               $this->assertContains( 'user:Frank!', $html );
+               $this->assertContains( 'time:20180101000003!', $html );
                $this->assertContains( 'Goats', $html );
 
                $rr->updateRevision( $savedRev ); // should do nothing
index ea195f1..28052ff 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace MediaWiki\Tests\Revision;
 
+use CommentStoreComment;
 use Content;
 use Language;
 use LogicException;
@@ -143,6 +144,7 @@ class RevisionRendererTest extends MediaWikiTestCase {
                $rev = new MutableRevisionRecord( $title );
                $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
                $rev->setTimestamp( '20180101000003' );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
 
                $text = "";
                $text .= "* page:{{PAGENAME}}\n";
@@ -179,6 +181,7 @@ class RevisionRendererTest extends MediaWikiTestCase {
                $rev->setId( 21 ); // current!
                $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
                $rev->setTimestamp( '20180101000003' );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
 
                $text = "";
                $text .= "* page:{{PAGENAME}}\n";
@@ -214,6 +217,7 @@ class RevisionRendererTest extends MediaWikiTestCase {
                $rev->setId( 21 ); // current!
                $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
                $rev->setTimestamp( '20180101000003' );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
 
                $text = "";
                $text .= "* page:{{PAGENAME}}\n";
@@ -243,6 +247,7 @@ class RevisionRendererTest extends MediaWikiTestCase {
                $rev->setId( 11 ); // old!
                $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
                $rev->setTimestamp( '20180101000003' );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
 
                $text = "";
                $text .= "* page:{{PAGENAME}}\n";
@@ -279,6 +284,7 @@ class RevisionRendererTest extends MediaWikiTestCase {
                $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
                $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
                $rev->setTimestamp( '20180101000003' );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
 
                $text = "";
                $text .= "* page:{{PAGENAME}}\n";
@@ -303,6 +309,7 @@ class RevisionRendererTest extends MediaWikiTestCase {
                $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
                $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
                $rev->setTimestamp( '20180101000003' );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
 
                $text = "";
                $text .= "* page:{{PAGENAME}}\n";
@@ -341,6 +348,7 @@ class RevisionRendererTest extends MediaWikiTestCase {
                $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
                $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
                $rev->setTimestamp( '20180101000003' );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
 
                $text = "";
                $text .= "* page:{{PAGENAME}}\n";
@@ -381,6 +389,7 @@ class RevisionRendererTest extends MediaWikiTestCase {
                $rev = new MutableRevisionRecord( $title );
                $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
                $rev->setTimestamp( '20180101000003' );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
 
                $rev->setContent( 'main', new WikitextContent( '[[Kittens]]' ) );
                $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
index 43678f9..48bf4aa 100644 (file)
@@ -14,6 +14,7 @@ use MediaWiki\User\UserIdentityValue;
 use MediaWikiTestCase;
 use TextContent;
 use Title;
+use User;
 use WikitextContent;
 
 /**
@@ -53,6 +54,7 @@ class MutableRevisionRecordTest extends MediaWikiTestCase {
                $record->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
                $record->setComment( $comment );
                $record->setUser( $user );
+               $record->setTimestamp( '20101010000000' );
 
                return $record;
        }
@@ -294,4 +296,52 @@ class MutableRevisionRecordTest extends MediaWikiTestCase {
                $this->assertFalse( $record->hasSlot( 'c' ) );
        }
 
+       public function provideNotReadyForInsertion() {
+               /** @var Title $title */
+               $title = $this->getMock( Title::class );
+
+               /** @var User $user */
+               $user = $this->getMock( User::class );
+
+               /** @var CommentStoreComment $comment */
+               $comment = $this->getMockBuilder( CommentStoreComment::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $content = new TextContent( 'Test' );
+
+               $rev = new MutableRevisionRecord( $title );
+               yield 'empty' => [ $rev ];
+
+               $rev = new MutableRevisionRecord( $title );
+               $rev->setContent( 'main', $content );
+               $rev->setUser( $user );
+               $rev->setComment( $comment );
+               yield 'no timestamp' => [ $rev ];
+
+               $rev = new MutableRevisionRecord( $title );
+               $rev->setUser( $user );
+               $rev->setComment( $comment );
+               $rev->setTimestamp( '20101010000000' );
+               yield 'no content' => [ $rev ];
+
+               $rev = new MutableRevisionRecord( $title );
+               $rev->setContent( 'main', $content );
+               $rev->setComment( $comment );
+               $rev->setTimestamp( '20101010000000' );
+               yield 'no user' => [ $rev ];
+
+               $rev = new MutableRevisionRecord( $title );
+               $rev->setUser( $user );
+               $rev->setContent( 'main', $content );
+               $rev->setTimestamp( '20101010000000' );
+               yield 'no comment' => [ $rev ];
+       }
+
+       /**
+        * @dataProvider provideNotReadyForInsertion
+        */
+       public function testNotReadyForInsertion( $rev ) {
+               $this->assertFalse( $rev->isReadyForInsertion() );
+       }
 }
index eb048a7..df7ee72 100644 (file)
@@ -517,4 +517,9 @@ trait RevisionRecordTests {
                }
        }
 
+       public function testIsReadyForInsertion() {
+               $rev = $this->newRevision();
+               $this->assertTrue( $rev->isReadyForInsertion() );
+       }
+
 }
index 5307ca9..aac94b8 100644 (file)
@@ -17,9 +17,16 @@ use WANObjectCache;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\TestingAccessWrapper;
+use WikitextContent;
 
 class RevisionStoreTest extends MediaWikiTestCase {
 
+       private function useTextId() {
+               global $wgMultiContentRevisionSchemaMigrationStage;
+
+               return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD );
+       }
+
        /**
         * @param LoadBalancer $loadBalancer
         * @param SqlBlobStore $blobStore
@@ -411,6 +418,10 @@ class RevisionStoreTest extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
         */
        public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
+               if ( !$this->useTextId() ) {
+                       $this->markTestSkipped( 'No longer applicable with MCR schema' );
+               }
+
                $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
                $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
 
@@ -432,6 +443,10 @@ class RevisionStoreTest extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
         */
        public function testNewRevisionFromRow_legacyEncoding_ignored() {
+               if ( !$this->useTextId() ) {
+                       $this->markTestSkipped( 'No longer applicable with MCR schema' );
+               }
+
                $row = [
                        'old_flags' => 'utf-8',
                        'old_text' => 'Söme Content',
@@ -457,7 +472,6 @@ class RevisionStoreTest extends MediaWikiTestCase {
                $row = $array + [
                                'rev_id' => 7,
                                'rev_page' => 5,
-                               'rev_text_id' => 11,
                                'rev_timestamp' => '20110101000000',
                                'rev_user_text' => 'Tester',
                                'rev_user' => 17,
@@ -469,8 +483,6 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                'rev_comment_text' => 'Testing',
                                'rev_comment_data' => '{}',
                                'rev_comment_cid' => 111,
-                               'rev_content_format' => CONTENT_FORMAT_TEXT,
-                               'rev_content_model' => CONTENT_MODEL_TEXT,
                                'page_namespace' => 0,
                                'page_title' => 'TEST',
                                'page_id' => 5,
@@ -478,10 +490,24 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                'page_is_redirect' => 0,
                                'page_len' => 100,
                                'user_name' => 'Tester',
-                               'old_is' => 13,
+                       ];
+
+               if ( $this->useTextId() ) {
+                       $row += [
+                               'rev_content_format' => CONTENT_FORMAT_TEXT,
+                               'rev_content_model' => CONTENT_MODEL_TEXT,
+                               'rev_text_id' => 11,
+                               'old_id' => 11,
                                'old_text' => 'Hello World',
                                'old_flags' => 'utf-8',
                        ];
+               } else {
+                       if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
+                               $row['content'] = [
+                                       'main' => new WikitextContent( $array['old_text'] ),
+                               ];
+                       }
+               }
 
                return (object)$row;
        }
index b01a7db..d69cfd1 100644 (file)
@@ -13,6 +13,10 @@ class TextContentTest extends MediaWikiLangTestCase {
        protected function setUp() {
                parent::setUp();
 
+               // trigger purging of all page related tables
+               $this->tablesUsed[] = 'page';
+               $this->tablesUsed[] = 'revision';
+
                // Anon user
                $user = new User();
                $user->setName( '127.0.0.1' );
@@ -352,11 +356,11 @@ class TextContentTest extends MediaWikiLangTestCase {
 
        public static function dataGetDeletionUpdates() {
                return [
-                       [ "TextContentTest_testGetSecondaryDataUpdates_1",
+                       [
                                CONTENT_MODEL_TEXT, "hello ''world''\n",
                                []
                        ],
-                       [ "TextContentTest_testGetSecondaryDataUpdates_2",
+                       [
                                CONTENT_MODEL_TEXT, "hello [[world test 21344]]\n",
                                []
                        ],
@@ -368,13 +372,11 @@ class TextContentTest extends MediaWikiLangTestCase {
         * @dataProvider dataGetDeletionUpdates
         * @covers TextContent::getDeletionUpdates
         */
-       public function testDeletionUpdates( $title, $model, $text, $expectedStuff ) {
-               $ns = $this->getDefaultWikitextNS();
-               $title = Title::newFromText( $title, $ns );
+       public function testDeletionUpdates( $model, $text, $expectedStuff ) {
+               $page = $this->getNonexistingTestPage( get_class( $this ) . '-' . $this->getName() );
+               $title = $page->getTitle();
 
                $content = ContentHandler::makeContent( $text, $title, $model );
-
-               $page = WikiPage::factory( $title );
                $page->doEditContent( $content, '' );
 
                $updates = $content->getDeletionUpdates( $page );
@@ -385,11 +387,6 @@ class TextContentTest extends MediaWikiLangTestCase {
                        $updates[$class] = $update;
                }
 
-               if ( !$expectedStuff ) {
-                       $this->assertTrue( true ); // make phpunit happy
-                       return;
-               }
-
                foreach ( $expectedStuff as $class => $fieldValues ) {
                        $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" );
 
@@ -401,7 +398,8 @@ class TextContentTest extends MediaWikiLangTestCase {
                        }
                }
 
-               $page->doDeleteArticle( '' );
+               // make phpunit happy even if $expectedStuff was empty
+               $this->assertTrue( true );
        }
 
        public static function provideConvert() {
index 91255eb..be93563 100644 (file)
@@ -431,11 +431,11 @@ just a test"
 
        public static function dataGetDeletionUpdates() {
                return [
-                       [ "WikitextContentTest_testGetSecondaryDataUpdates_1",
+                       [
                                CONTENT_MODEL_WIKITEXT, "hello ''world''\n",
                                [ LinksDeletionUpdate::class => [] ]
                        ],
-                       [ "WikitextContentTest_testGetSecondaryDataUpdates_2",
+                       [
                                CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n",
                                [ LinksDeletionUpdate::class => [] ]
                        ],
index d721274..68cddd6 100644 (file)
@@ -425,6 +425,44 @@ class ArticleViewTest extends MediaWikiTestCase {
                        }
                );
 
+               $this->hideDeprecated(
+                       'ArticleContentViewCustom hook (used in hook-ArticleContentViewCustom-closure)'
+               );
+
+               $article->view();
+
+               $output = $article->getContext()->getOutput();
+               $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
+               $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
+       }
+
+       public function testArticleRevisionViewCustomHook() {
+               $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] );
+
+               $article = new Article( $page->getTitle(), 0 );
+               $article->getContext()->getOutput()->setTitle( $page->getTitle() );
+
+               // use ArticleViewHeader hook to bypass the parser cache
+               $this->setTemporaryHook(
+                       'ArticleViewHeader',
+                       function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
+                               $useParserCache = false;
+                       }
+               );
+
+               $this->setTemporaryHook(
+                       'ArticleRevisionViewCustom',
+                       function ( RevisionRecord $rev, Title $title, $oldid, OutputPage $output ) use ( $page ) {
+                               $content = $rev->getContent( 'main' );
+
+                               $this->assertSame( $page->getTitle(), $title, '$title' );
+                               $this->assertSame( 'Test A', $content->getNativeData(), '$content' );
+
+                               $output->addHTML( 'Hook Text' );
+                               return false;
+                       }
+               );
+
                $article->view();
 
                $output = $article->getContext()->getOutput();
@@ -456,6 +494,11 @@ class ArticleViewTest extends MediaWikiTestCase {
                        }
                );
 
+               $this->hideDeprecated(
+                       'ArticleAfterFetchContentObject hook'
+                       . ' (used in hook-ArticleAfterFetchContentObject-closure)'
+               );
+
                $article->view();
 
                $output = $article->getContext()->getOutput();
diff --git a/tests/phpunit/includes/poolcounter/PoolWorkArticleViewTest.php b/tests/phpunit/includes/poolcounter/PoolWorkArticleViewTest.php
new file mode 100644 (file)
index 0000000..61eb316
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionRecord;
+
+/**
+ * @covers PoolWorkArticleView
+ */
+class PoolWorkArticleViewTest extends MediaWikiTestCase {
+
+       private function makeRevision( WikiPage $page, $text ) {
+               $user = $this->getTestUser()->getUser();
+               $updater = $page->newPageUpdater( $user );
+
+               $updater->setContent( 'main', new WikitextContent( $text ) );
+               return $updater->saveRevision( CommentStoreComment::newUnsavedComment( 'testing' ) );
+       }
+
+       public function testDoWorkLoadRevision() {
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $page = $this->getExistingTestPage( __METHOD__ );
+               $rev1 = $this->makeRevision( $page, 'First!' );
+               $rev2 = $this->makeRevision( $page, 'Second!' );
+
+               $work = new PoolWorkArticleView( $page, $options, $rev1->getId(), false );
+               $work->execute();
+               $this->assertContains( 'First', $work->getParserOutput()->getText() );
+
+               $work = new PoolWorkArticleView( $page, $options, $rev2->getId(), false );
+               $work->execute();
+               $this->assertContains( 'Second', $work->getParserOutput()->getText() );
+       }
+
+       public function testDoWorkParserCache() {
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $page = $this->getExistingTestPage( __METHOD__ );
+               $rev1 = $this->makeRevision( $page, 'First!' );
+
+               $work = new PoolWorkArticleView( $page, $options, $rev1->getId(), true );
+               $work->execute();
+
+               $cache = MediaWikiServices::getInstance()->getParserCache();
+               $out = $cache->get( $page, $options );
+
+               $this->assertNotNull( $out );
+               $this->assertNotFalse( $out );
+               $this->assertContains( 'First', $out->getText() );
+       }
+
+       public function testDoWorkWithExplicitRevision() {
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $page = $this->getExistingTestPage( __METHOD__ );
+               $rev = $this->makeRevision( $page, 'NOPE' );
+
+               // make a fake revision with different content, so we know it's actually being used!
+               $fakeRev = new MutableRevisionRecord( $page->getTitle() );
+               $fakeRev->setId( $rev->getId() );
+               $fakeRev->setPageId( $page->getId() );
+               $fakeRev->setContent( 'main', new WikitextContent( 'YES!' ) );
+
+               $work = new PoolWorkArticleView( $page, $options, $rev->getId(), false, $fakeRev );
+               $work->execute();
+
+               $text = $work->getParserOutput()->getText();
+               $this->assertContains( 'YES!', $text );
+               $this->assertNotContains( 'NOPE', $text );
+       }
+
+       public function testDoWorkWithContent() {
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $page = $this->getExistingTestPage( __METHOD__ );
+
+               $content = new WikitextContent( 'YES!' );
+
+               $work = new PoolWorkArticleView( $page, $options, $page->getLatest(), false, $content );
+               $work->execute();
+
+               $text = $work->getParserOutput()->getText();
+               $this->assertContains( 'YES!', $text );
+       }
+
+       public function testDoWorkWithString() {
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $page = $this->getExistingTestPage( __METHOD__ );
+
+               $work = new PoolWorkArticleView( $page, $options, $page->getLatest(), false, 'YES!' );
+               $work->execute();
+
+               $text = $work->getParserOutput()->getText();
+               $this->assertContains( 'YES!', $text );
+       }
+
+       public function provideMagicWords() {
+               yield 'PAGEID' => [
+                       'Test {{PAGEID}} Test',
+                       function ( RevisionRecord $rev ) {
+                               return $rev->getPageId();
+                       }
+               ];
+               yield 'REVISIONID' => [
+                       'Test {{REVISIONID}} Test',
+                       function ( RevisionRecord $rev ) {
+                               return $rev->getId();
+                       }
+               ];
+               yield 'REVISIONUSER' => [
+                       'Test {{REVISIONUSER}} Test',
+                       function ( RevisionRecord $rev ) {
+                               return $rev->getUser()->getName();
+                       }
+               ];
+               yield 'REVISIONTIMESTAMP' => [
+                       'Test {{REVISIONTIMESTAMP}} Test',
+                       function ( RevisionRecord $rev ) {
+                               return $rev->getTimestamp();
+                       }
+               ];
+       }
+       /**
+        * @dataProvider provideMagicWords
+        */
+       public function testMagicWords( $wikitext, $callback ) {
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $page = $this->getExistingTestPage( __METHOD__ );
+               $rev = $page->getRevision()->getRevisionRecord();
+
+               // NOTE: provide the input as a string and let the PoolWorkArticleView create a fake
+               // revision internally, to see if the magic words work with that fake. They should
+               // work if the Parser causes the actual revision to be loaded when needed.
+               $work = new PoolWorkArticleView( $page, $options, $page->getLatest(), false, $wikitext );
+               $work->execute();
+
+               $expected = strval( $callback( $rev ) );
+               $output = $work->getParserOutput();
+
+               $this->assertContains( $expected, $output->getText() );
+       }
+
+       public function testDoWorkMissingPage() {
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $page = $this->getNonexistingTestPage();
+
+               $work = new PoolWorkArticleView( $page, $options, '667788', false );
+               $this->assertFalse( $work->execute() );
+       }
+
+       public function testDoWorkDeletedContent() {
+               $options = ParserOptions::newCanonical( 'canonical' );
+               $page = $this->getExistingTestPage( __METHOD__ );
+               $rev1 = $page->getRevision()->getRevisionRecord();
+
+               // make another revision, since the latest revision cannot be deleted.
+               $rev2 = $this->makeRevision( $page, 'Next' );
+
+               // make a fake revision with deleted different content
+               $fakeRev = new MutableRevisionRecord( $page->getTitle() );
+               $fakeRev->setId( $rev1->getId() );
+               $fakeRev->setPageId( $page->getId() );
+               $fakeRev->setContent( 'main', new WikitextContent( 'SECRET' ) );
+               $fakeRev->setVisibility( RevisionRecord::DELETED_TEXT );
+
+               $work = new PoolWorkArticleView( $page, $options, $rev1->getId(), false, $fakeRev );
+               $this->assertFalse( $work->execute() );
+
+               // a deleted current revision should still be show
+               $fakeRev->setId( $rev2->getId() );
+               $work = new PoolWorkArticleView( $page, $options, $rev2->getId(), false, $fakeRev );
+               $this->assertNotFalse( $work->execute() );
+       }
+
+}