From: jenkins-bot Date: Tue, 11 Sep 2018 18:13:00 +0000 (+0000) Subject: Merge "Remove xx-uca-et collation workaround" X-Git-Tag: 1.34.0-rc.0~4128 X-Git-Url: http://git.cyclocoop.org/%7B%24admin_url%7Dmes_infos.php?a=commitdiff_plain;h=24107dd6b07277f0e1cd52815c7de66209c86a0e;hp=9c46871d60f02a033c6082836f9b6a18b3a10ec1;p=lhc%2Fweb%2Fwiklou.git Merge "Remove xx-uca-et collation workaround" --- diff --git a/docs/hooks.txt b/docs/hooks.txt index 436131cdd8..cce50e058c 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -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 diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 702ea54f6a..343e80d6f5 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -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. diff --git a/includes/Revision.php b/includes/Revision.php index 6d684a8052..1e35ddaeaf 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -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 = [ diff --git a/includes/Revision/RenderedRevision.php b/includes/Revision/RenderedRevision.php index 0c052d1b46..ba40a81f14 100644 --- a/includes/Revision/RenderedRevision.php +++ b/includes/Revision/RenderedRevision.php @@ -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 ); + } } - } - ); + ); + } } /** diff --git a/includes/Storage/RevisionArchiveRecord.php b/includes/Storage/RevisionArchiveRecord.php index 213ee3cd1b..173da51907 100644 --- a/includes/Storage/RevisionArchiveRecord.php +++ b/includes/Storage/RevisionArchiveRecord.php @@ -167,4 +167,13 @@ class RevisionArchiveRecord extends RevisionRecord { return parent::getTimestamp(); } + /** + * @see RevisionStore::isComplete + * + * @return bool always true. + */ + public function isReadyForInsertion() { + return true; + } + } diff --git a/includes/Storage/RevisionRecord.php b/includes/Storage/RevisionRecord.php index 17c56ea0ea..8c31a3caf7 100644 --- a/includes/Storage/RevisionRecord.php +++ b/includes/Storage/RevisionRecord.php @@ -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() !== []; + } + } diff --git a/includes/Storage/RevisionStore.php b/includes/Storage/RevisionStore.php index ce8a0883f6..8e66906287 100644 --- a/includes/Storage/RevisionStore.php +++ b/includes/Storage/RevisionStore.php @@ -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 diff --git a/includes/Storage/RevisionStoreRecord.php b/includes/Storage/RevisionStoreRecord.php index d092f22ed9..6148c44366 100644 --- a/includes/Storage/RevisionStoreRecord.php +++ b/includes/Storage/RevisionStoreRecord.php @@ -207,4 +207,13 @@ class RevisionStoreRecord extends RevisionRecord { return parent::getTimestamp(); } + /** + * @see RevisionStore::isComplete + * + * @return bool always true. + */ + public function isReadyForInsertion() { + return true; + } + } diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php index 344d0406bd..7a378b3bea 100644 --- a/includes/content/ContentHandler.php +++ b/includes/content/ContentHandler.php @@ -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 ); } diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 1d69f12ec6..387e9e3c18 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -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() ); diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 7920e9cb4b..fa6e180ee1 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -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(); } diff --git a/includes/jobqueue/jobs/CategoryMembershipChangeJob.php b/includes/jobqueue/jobs/CategoryMembershipChangeJob.php index a0c70abe71..c39823ff9b 100644 --- a/includes/jobqueue/jobs/CategoryMembershipChangeJob.php +++ b/includes/jobqueue/jobs/CategoryMembershipChangeJob.php @@ -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! diff --git a/includes/page/Article.php b/includes/page/Article.php index e90334fce6..464bb608ce 100644 --- a/includes/page/Article.php +++ b/includes/page/Article.php @@ -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 { "
\n$1\n
", '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( "\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( "" . - $this->getContext()->msg( 'redirectpagesub' )->parse() . "" ); + if ( $this->mParserOutput ) { + $outputPage->addParserOutput( $this->mParserOutput, $poOptions ); } - # Don't cache a dirty ParserOutput object - if ( $poolArticleView->getIsDirty() ) { - $outputPage->setCdnMaxage( 0 ); - $outputPage->addHTML( "\n" ); + if ( $rev && $this->getRevisionRedirectTarget( $rev ) ) { + $outputPage->addSubtitle( "" . + $this->getContext()->msg( 'redirectpagesub' )->parse() . "" ); } $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( '' . $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 ) { diff --git a/includes/page/ImagePage.php b/includes/page/ImagePage.php index 58f25d4650..d3d6da7935 100644 --- a/includes/page/ImagePage.php +++ b/includes/page/ImagePage.php @@ -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 ) { diff --git a/includes/poolcounter/PoolWorkArticleView.php b/includes/poolcounter/PoolWorkArticleView.php index 4af86ae03d..286494efa4 100644 --- a/includes/poolcounter/PoolWorkArticleView.php +++ b/includes/poolcounter/PoolWorkArticleView.php @@ -17,7 +17,12 @@ * * @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 diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index 3069bd8654..a92982040b 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -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', diff --git a/maintenance/compareParserCache.php b/maintenance/compareParserCache.php index b12974b06e..2cafc1b31a 100644 --- a/maintenance/compareParserCache.php +++ b/maintenance/compareParserCache.php @@ -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; diff --git a/tests/phpunit/includes/Revision/RenderedRevisionTest.php b/tests/phpunit/includes/Revision/RenderedRevisionTest.php index a2a9d09322..bea0b494ef 100644 --- a/tests/phpunit/includes/Revision/RenderedRevisionTest.php +++ b/tests/phpunit/includes/Revision/RenderedRevisionTest.php @@ -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)(TWO)#{{:$name}}#"; - $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 diff --git a/tests/phpunit/includes/Revision/RevisionRendererTest.php b/tests/phpunit/includes/Revision/RevisionRendererTest.php index ea195f1087..28052ff017 100644 --- a/tests/phpunit/includes/Revision/RevisionRendererTest.php +++ b/tests/phpunit/includes/Revision/RevisionRendererTest.php @@ -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]]' ) ); diff --git a/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php b/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php index 43678f9d98..48bf4aa665 100644 --- a/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php +++ b/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php @@ -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() ); + } } diff --git a/tests/phpunit/includes/Storage/RevisionRecordTests.php b/tests/phpunit/includes/Storage/RevisionRecordTests.php index eb048a7edc..df7ee7276c 100644 --- a/tests/phpunit/includes/Storage/RevisionRecordTests.php +++ b/tests/phpunit/includes/Storage/RevisionRecordTests.php @@ -517,4 +517,9 @@ trait RevisionRecordTests { } } + public function testIsReadyForInsertion() { + $rev = $this->newRevision(); + $this->assertTrue( $rev->isReadyForInsertion() ); + } + } diff --git a/tests/phpunit/includes/Storage/RevisionStoreTest.php b/tests/phpunit/includes/Storage/RevisionStoreTest.php index 5307ca9233..aac94b8127 100644 --- a/tests/phpunit/includes/Storage/RevisionStoreTest.php +++ b/tests/phpunit/includes/Storage/RevisionStoreTest.php @@ -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; } diff --git a/tests/phpunit/includes/content/TextContentTest.php b/tests/phpunit/includes/content/TextContentTest.php index b01a7db04a..d69cfd1a86 100644 --- a/tests/phpunit/includes/content/TextContentTest.php +++ b/tests/phpunit/includes/content/TextContentTest.php @@ -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() { diff --git a/tests/phpunit/includes/content/WikitextContentTest.php b/tests/phpunit/includes/content/WikitextContentTest.php index 91255eb0f5..be93563168 100644 --- a/tests/phpunit/includes/content/WikitextContentTest.php +++ b/tests/phpunit/includes/content/WikitextContentTest.php @@ -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 => [] ] ], diff --git a/tests/phpunit/includes/page/ArticleViewTest.php b/tests/phpunit/includes/page/ArticleViewTest.php index d7212746b8..68cddd6cc9 100644 --- a/tests/phpunit/includes/page/ArticleViewTest.php +++ b/tests/phpunit/includes/page/ArticleViewTest.php @@ -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 index 0000000000..61eb316c55 --- /dev/null +++ b/tests/phpunit/includes/poolcounter/PoolWorkArticleViewTest.php @@ -0,0 +1,171 @@ +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() ); + } + +}