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
$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
$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
* @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 );
}
/**
* 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
$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 ) ) {
* @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() );
* @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();
}
* @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!
* @file
*/
use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionRecord;
/**
* Class for viewing MediaWiki article and history.
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;
/**
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. */
/** @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
*/
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() );
}
$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();
}
* 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;
}
/**
- * @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 ) ) {
}
}
+ $this->mRevIdFetched = $this->mRevision ? $this->mRevision->getId() : 0;
+
return $oldid;
}
* 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().
* @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;
}
/**
/**
* 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();
}
}
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() ) {
"<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;
# 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 );
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;
}
}
- # 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
$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
# 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
# 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();
__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() .
/**
* Output deletion confirmation dialog
- * @todo FIXME: Move to another file?
+ * @todo Move to another file?
* @param string $reason Prefilled reason
*/
public function confirmDelete( $reason ) {
}
/**
- * 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 ) {
*
* @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 */
/** @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;
* @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 );
}
$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;
}
$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
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\RevisionRecord;
use Wikimedia\Rdbms\IResultWrapper;
/**
$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 ) {
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,
] );
$buttonFields = [];
if ( $isText ) {
+ // TODO: MCR: make this work for multiple slots
// source view for textual content
$sourceView = Xml::element( 'textarea', [
'readonly' => 'readonly',
$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
$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;
}
);
+ $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();
}
);
+ $this->hideDeprecated(
+ 'ArticleAfterFetchContentObject hook'
+ . ' (used in hook-ArticleAfterFetchContentObject-closure)'
+ );
+
$article->view();
$output = $article->getContext()->getOutput();
--- /dev/null
+<?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() );
+ }
+
+}