X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Fpage%2FArticle.php;h=6a42d58c287bfd890796e490a663a0352a0f70d7;hp=49912c77ddf3cc220dbb08100bb6ffadefb35fee;hb=4df0c71911500466a6330b8fe29c623ef5b51e41;hpb=925c20a9ca669c67cc44f4468d0e0f3b33b94213 diff --git a/includes/page/Article.php b/includes/page/Article.php index 49912c77dd..6a42d58c28 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. @@ -33,47 +35,84 @@ use MediaWiki\MediaWikiServices; * moved to separate EditPage and HTMLFileCache classes. */ class Article implements Page { - /** @var IContextSource The context this Article is executed in */ + /** + * @var IContextSource|null The context this Article is executed in. + * 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; - /** @var ParserOptions ParserOptions object for $wgUser articles */ - public $mParserOptions; - /** - * @var string Text of the revision we are working on - * @todo BC cruft + * @var ParserOptions|null ParserOptions object for $wgUser articles. + * Initialized by getParserOptions by calling $this->mPage->makeParserOptions(). */ - public $mContent; + public $mParserOptions; /** - * @var Content Content of the revision we are working on + * @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 Title from which we were redirected here */ + /** @var Title|null Title from which we were redirected here, if any. */ public $mRedirectedFrom = null; /** @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 Revision we are working on */ + /** + * @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 */ - public $mParserOutput; + /** + * @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 = null; /** * @var bool Whether render() was called. With the way subclasses work @@ -119,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() ); } @@ -201,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(); } @@ -216,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; @@ -244,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 ) ) { @@ -279,7 +355,7 @@ class Article implements Page { if ( $this->mRevision !== null ) { // Revision title doesn't match the page title given? if ( $this->mPage->getId() != $this->mRevision->getPage() ) { - $function = get_class( $this->mPage ). '::newFromID'; + $function = get_class( $this->mPage ) . '::newFromID'; $this->mPage = $function( $this->mRevision->getPage() ); } } @@ -302,6 +378,8 @@ class Article implements Page { } } + $this->mRevIdFetched = $this->mRevision ? $this->mRevision->getId() : 0; + return $oldid; } @@ -309,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(). @@ -318,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 ( $oldid ) { - # $this->mRevision might already be fetched by getOldIDFromRequest() - if ( !$this->mRevision ) { + if ( !$this->mRevision ) { + wfDebug( __METHOD__ . " failed to find page data for title " . + $this->getTitle()->getPrefixedText() . "\n" ); + + // 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; } /** @@ -404,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(); } @@ -558,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() ) { @@ -586,10 +735,21 @@ class Article implements Page { "