From: withoutaname Date: Sun, 22 Jun 2014 01:29:03 +0000 (-0700) Subject: Move implementations of Page to separate file X-Git-Tag: 1.31.0-rc.0~15300 X-Git-Url: http://git.cyclocoop.org/%22.%28%24lien.?a=commitdiff_plain;h=32ac3913310fe9783458db13929b2caf416cfdc8;p=lhc%2Fweb%2Fwiklou.git Move implementations of Page to separate file Moved implementations of the Page interface, including subclasses of WikiPage and Article, to a separate /includes/page file. Separated PoolWorkArticleView to the includes/poolcounter file. Change-Id: I4557eab76e0cb12d9d7f93644c5831bdd5b472b0 --- diff --git a/includes/Article.php b/includes/Article.php deleted file mode 100644 index c68c675dc9..0000000000 --- a/includes/Article.php +++ /dev/null @@ -1,2100 +0,0 @@ -mOldId = $oldId; - $this->mPage = $this->newPage( $title ); - } - - /** - * @param Title $title - * @return WikiPage - */ - protected function newPage( Title $title ) { - return new WikiPage( $title ); - } - - /** - * Constructor from a page id - * @param int $id Article ID to load - * @return Article|null - */ - public static function newFromID( $id ) { - $t = Title::newFromID( $id ); - # @todo FIXME: Doesn't inherit right - return $t == null ? null : new self( $t ); - # return $t == null ? null : new static( $t ); // PHP 5.3 - } - - /** - * Create an Article object of the appropriate class for the given page. - * - * @param Title $title - * @param IContextSource $context - * @return Article - */ - public static function newFromTitle( $title, IContextSource $context ) { - if ( NS_MEDIA == $title->getNamespace() ) { - // FIXME: where should this go? - $title = Title::makeTitle( NS_FILE, $title->getDBkey() ); - } - - $page = null; - wfRunHooks( 'ArticleFromTitle', array( &$title, &$page, $context ) ); - if ( !$page ) { - switch ( $title->getNamespace() ) { - case NS_FILE: - $page = new ImagePage( $title ); - break; - case NS_CATEGORY: - $page = new CategoryPage( $title ); - break; - default: - $page = new Article( $title ); - } - } - $page->setContext( $context ); - - return $page; - } - - /** - * Create an Article object of the appropriate class for the given page. - * - * @param WikiPage $page - * @param IContextSource $context - * @return Article - */ - public static function newFromWikiPage( WikiPage $page, IContextSource $context ) { - $article = self::newFromTitle( $page->getTitle(), $context ); - $article->mPage = $page; // override to keep process cached vars - return $article; - } - - /** - * Tell the page view functions that this view was redirected - * from another page on the wiki. - * @param Title $from - */ - public function setRedirectedFrom( Title $from ) { - $this->mRedirectedFrom = $from; - } - - /** - * Get the title object of the article - * - * @return Title Title object of this page - */ - public function getTitle() { - return $this->mPage->getTitle(); - } - - /** - * Get the WikiPage object of this instance - * - * @since 1.19 - * @return WikiPage - */ - public function getPage() { - return $this->mPage; - } - - /** - * Clear the object - */ - public function clear() { - $this->mContentLoaded = false; - - $this->mRedirectedFrom = null; # Title object if set - $this->mRevIdFetched = 0; - $this->mRedirectUrl = false; - - $this->mPage->clear(); - } - - /** - * Note that getContent/loadContent do not follow redirects anymore. - * If you need to fetch redirectable content easily, try - * the shortcut in WikiPage::getRedirectTarget() - * - * This function has side effects! Do not use this function if you - * only want the real revision text if any. - * - * @deprecated since 1.21; use WikiPage::getContent() instead - * - * @return string Return the text of this revision - */ - public function getContent() { - ContentHandler::deprecated( __METHOD__, '1.21' ); - $content = $this->getContentObject(); - return ContentHandler::getContentText( $content ); - } - - /** - * Returns a Content object representing the pages effective display content, - * not necessarily the revision's content! - * - * Note that getContent/loadContent do not follow redirects anymore. - * If you need to fetch redirectable content easily, try - * the shortcut in WikiPage::getRedirectTarget() - * - * 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 - * - * @since 1.21 - */ - protected function getContentObject() { - wfProfileIn( __METHOD__ ); - - 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' ); - } - } else { - $this->fetchContentObject(); - $content = $this->mContentObject; - } - - wfProfileOut( __METHOD__ ); - return $content; - } - - /** - * @return int The oldid of the article that is to be shown, 0 for the current revision - */ - public function getOldID() { - if ( is_null( $this->mOldId ) ) { - $this->mOldId = $this->getOldIDFromRequest(); - } - - return $this->mOldId; - } - - /** - * Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect - * - * @return int The old id for the request - */ - public function getOldIDFromRequest() { - $this->mRedirectUrl = false; - - $request = $this->getContext()->getRequest(); - $oldid = $request->getIntOrNull( 'oldid' ); - - if ( $oldid === null ) { - return 0; - } - - if ( $oldid !== 0 ) { - # Load the given revision and check whether the page is another one. - # In that case, update this instance to reflect the change. - if ( $oldid === $this->mPage->getLatest() ) { - $this->mRevision = $this->mPage->getRevision(); - } else { - $this->mRevision = Revision::newFromId( $oldid ); - if ( $this->mRevision !== null ) { - // Revision title doesn't match the page title given? - if ( $this->mPage->getID() != $this->mRevision->getPage() ) { - $function = array( get_class( $this->mPage ), 'newFromID' ); - $this->mPage = call_user_func( $function, $this->mRevision->getPage() ); - } - } - } - } - - if ( $request->getVal( 'direction' ) == 'next' ) { - $nextid = $this->getTitle()->getNextRevisionID( $oldid ); - if ( $nextid ) { - $oldid = $nextid; - $this->mRevision = null; - } else { - $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' ); - } - } elseif ( $request->getVal( 'direction' ) == 'prev' ) { - $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); - if ( $previd ) { - $oldid = $previd; - $this->mRevision = null; - } - } - - return $oldid; - } - - /** - * Load the revision (including text) into this object - * - * @deprecated since 1.19; use fetchContent() - */ - function loadContent() { - wfDeprecated( __METHOD__, '1.19' ); - $this->fetchContent(); - } - - /** - * Get text of an article from database - * Does *NOT* follow redirects. - * - * @protected - * @note This is really internal functionality that should really NOT be - * used by other functions. For accessing article content, use the WikiPage - * class, especially WikiBase::getContent(). However, a lot of legacy code - * uses this method to retrieve page text from the database, so the function - * has to remain public for now. - * - * @return string|bool String containing article contents, or false if null - * @deprecated since 1.21, use WikiPage::getContent() instead - */ - function fetchContent() { #BC cruft! - ContentHandler::deprecated( __METHOD__, '1.21' ); - - if ( $this->mContentLoaded && $this->mContent ) { - return $this->mContent; - } - - wfProfileIn( __METHOD__ ); - - $content = $this->fetchContentObject(); - - if ( !$content ) { - wfProfileOut( __METHOD__ ); - return false; - } - - // @todo Get rid of mContent everywhere! - $this->mContent = ContentHandler::getContentText( $content ); - ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); - - wfProfileOut( __METHOD__ ); - - return $this->mContent; - } - - /** - * Get text content object - * Does *NOT* follow redirects. - * @todo When is this null? - * - * @note Code that wants to retrieve page content from the database should - * use WikiPage::getContent(). - * - * @return Content|null|bool - * - * @since 1.21 - */ - protected function fetchContentObject() { - if ( $this->mContentLoaded ) { - return $this->mContentObject; - } - - wfProfileIn( __METHOD__ ); - - $this->mContentLoaded = true; - $this->mContent = 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', array( $oldid ), array() ); - - if ( $oldid ) { - # $this->mRevision might already be fetched by getOldIDFromRequest() - if ( !$this->mRevision ) { - $this->mRevision = Revision::newFromId( $oldid ); - if ( !$this->mRevision ) { - wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" ); - wfProfileOut( __METHOD__ ); - return false; - } - } - } else { - if ( !$this->mPage->getLatest() ) { - wfDebug( __METHOD__ . " failed to find page data for title " . - $this->getTitle()->getPrefixedText() . "\n" ); - wfProfileOut( __METHOD__ ); - return false; - } - - $this->mRevision = $this->mPage->getRevision(); - - if ( !$this->mRevision ) { - wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . - $this->mPage->getLatest() . "\n" ); - wfProfileOut( __METHOD__ ); - return false; - } - } - - // @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 - $this->mContentObject = $this->mRevision->getContent( - Revision::FOR_THIS_USER, - $this->getContext()->getUser() - ); - $this->mRevIdFetched = $this->mRevision->getId(); - - wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) ); - - wfProfileOut( __METHOD__ ); - - return $this->mContentObject; - } - - /** - * Returns true if the currently-referenced revision is the current edit - * to this page (and it exists). - * @return bool - */ - public function isCurrent() { - # If no oldid, this is the current version. - if ( $this->getOldID() == 0 ) { - return true; - } - - return $this->mPage->exists() && $this->mRevision && $this->mRevision->isCurrent(); - } - - /** - * Get the fetched Revision object depending on request parameters or null - * on failure. - * - * @since 1.19 - * @return Revision|null - */ - public function getRevisionFetched() { - $this->fetchContentObject(); - - return $this->mRevision; - } - - /** - * Use this to fetch the rev ID used on page views - * - * @return int Revision ID of last article revision - */ - public function getRevIdFetched() { - if ( $this->mRevIdFetched ) { - return $this->mRevIdFetched; - } else { - return $this->mPage->getLatest(); - } - } - - /** - * This is the default action of the index.php entry point: just view the - * page of the given title. - */ - public function view() { - global $wgUseFileCache, $wgUseETag, $wgDebugToolbar; - - wfProfileIn( __METHOD__ ); - - # Get variables from query string - # As side effect this will load the revision and update the title - # in a revision ID is passed in the request, so this should remain - # the first call of this method even if $oldid is used way below. - $oldid = $this->getOldID(); - - $user = $this->getContext()->getUser(); - # Another whitelist check in case getOldID() is altering the title - $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $user ); - if ( count( $permErrors ) ) { - wfDebug( __METHOD__ . ": denied on secondary read check\n" ); - wfProfileOut( __METHOD__ ); - throw new PermissionsError( 'read', $permErrors ); - } - - $outputPage = $this->getContext()->getOutput(); - # getOldID() may as well want us to redirect somewhere else - if ( $this->mRedirectUrl ) { - $outputPage->redirect( $this->mRedirectUrl ); - wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); - wfProfileOut( __METHOD__ ); - - return; - } - - # If we got diff in the query, we want to see a diff page instead of the article. - if ( $this->getContext()->getRequest()->getCheck( 'diff' ) ) { - wfDebug( __METHOD__ . ": showing diff page\n" ); - $this->showDiffPage(); - wfProfileOut( __METHOD__ ); - - return; - } - - # Set page title (may be overridden by DISPLAYTITLE) - $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() ); - - $outputPage->setArticleFlag( true ); - # Allow frames by default - $outputPage->allowClickjacking(); - - $parserCache = ParserCache::singleton(); - - $parserOptions = $this->getParserOptions(); - # Render printable version, use printable version cache - if ( $outputPage->isPrintable() ) { - $parserOptions->setIsPrintable( true ); - $parserOptions->setEditSection( false ); - } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) ) { - $parserOptions->setEditSection( false ); - } - - # Try client and file cache - if ( !$wgDebugToolbar && $oldid === 0 && $this->mPage->checkTouched() ) { - if ( $wgUseETag ) { - $outputPage->setETag( $parserCache->getETag( $this, $parserOptions ) ); - } - - # Is it client cached? - if ( $outputPage->checkLastModified( $this->mPage->getTouched() ) ) { - wfDebug( __METHOD__ . ": done 304\n" ); - wfProfileOut( __METHOD__ ); - - return; - # Try file cache - } elseif ( $wgUseFileCache && $this->tryFileCache() ) { - wfDebug( __METHOD__ . ": done file cache\n" ); - # tell wgOut that output is taken care of - $outputPage->disable(); - $this->mPage->doViewUpdates( $user, $oldid ); - wfProfileOut( __METHOD__ ); - - return; - } - } - - # Should the parser cache be used? - $useParserCache = $this->mPage->isParserCacheUsed( $parserOptions, $oldid ); - wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); - if ( $user->getStubThreshold() ) { - wfIncrStats( 'pcache_miss_stub' ); - } - - $this->showRedirectedFromHeader(); - $this->showNamespaceHeader(); - - # Iterate through the possible ways of constructing the output text. - # Keep going until $outputDone is set, or we run out of things to do. - $pass = 0; - $outputDone = false; - $this->mParserOutput = false; - - while ( !$outputDone && ++$pass ) { - switch ( $pass ) { - case 1: - wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$useParserCache ) ); - break; - case 2: - # Early abort if the page doesn't exist - if ( !$this->mPage->exists() ) { - wfDebug( __METHOD__ . ": showing missing article\n" ); - $this->showMissingArticle(); - $this->mPage->doViewUpdates( $user ); - wfProfileOut( __METHOD__ ); - return; - } - - # Try the parser cache - if ( $useParserCache ) { - $this->mParserOutput = $parserCache->get( $this, $parserOptions ); - - if ( $this->mParserOutput !== false ) { - if ( $oldid ) { - wfDebug( __METHOD__ . ": showing parser cache contents for current rev permalink\n" ); - $this->setOldSubtitle( $oldid ); - } else { - wfDebug( __METHOD__ . ": showing parser cache contents\n" ); - } - $outputPage->addParserOutput( $this->mParserOutput ); - # Ensure that UI elements requiring revision ID have - # the correct version information. - $outputPage->setRevisionId( $this->mPage->getLatest() ); - # Preload timestamp to avoid a DB hit - $cachedTimestamp = $this->mParserOutput->getTimestamp(); - if ( $cachedTimestamp !== null ) { - $outputPage->setRevisionTimestamp( $cachedTimestamp ); - $this->mPage->setTimestamp( $cachedTimestamp ); - } - $outputDone = true; - } - } - break; - case 3: - # This will set $this->mRevision if needed - $this->fetchContentObject(); - - # Are we looking at an old revision - if ( $oldid && $this->mRevision ) { - $this->setOldSubtitle( $oldid ); - - if ( !$this->showDeletedRevisionHeader() ) { - wfDebug( __METHOD__ . ": cannot view deleted revision\n" ); - wfProfileOut( __METHOD__ ); - return; - } - } - - # Ensure that UI elements requiring revision ID have - # the correct version information. - $outputPage->setRevisionId( $this->getRevIdFetched() ); - # Preload timestamp to avoid a DB hit - $outputPage->setRevisionTimestamp( $this->getTimestamp() ); - - # Pages containing custom CSS or JavaScript get special treatment - if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) { - wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); - $this->showCssOrJsPage(); - $outputDone = true; - } elseif ( !wfRunHooks( 'ArticleContentViewCustom', - array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) { - - # Allow extensions do their own custom view for certain pages - $outputDone = true; - } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', - array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) { - - # Allow extensions do their own custom view for certain pages - $outputDone = true; - } - break; - case 4: - # 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 ); - - if ( !$poolArticleView->execute() ) { - $error = $poolArticleView->getError(); - if ( $error ) { - $outputPage->clearHTML(); // for release() errors - $outputPage->enableClientCache( false ); - $outputPage->setRobotPolicy( 'noindex,nofollow' ); - - $errortext = $error->getWikiText( false, 'view-pool-error' ); - $outputPage->addWikiText( '
' . $errortext . '
' ); - } - # Connection or timeout error - wfProfileOut( __METHOD__ ); - return; - } - - $this->mParserOutput = $poolArticleView->getParserOutput(); - $outputPage->addParserOutput( $this->mParserOutput ); - if ( $content->getRedirectTarget() ) { - $outputPage->addSubtitle( wfMessage( 'redirectpagesub' )->parse() ); - } - - # Don't cache a dirty ParserOutput object - if ( $poolArticleView->getIsDirty() ) { - $outputPage->setSquidMaxage( 0 ); - $outputPage->addHTML( "\n" ); - } - - $outputDone = true; - break; - # Should be unreachable, but just in case... - default: - break 2; - } - } - - # Get the ParserOutput actually *displayed* here. - # Note that $this->mParserOutput is the *current* version output. - $pOutput = ( $outputDone instanceof ParserOutput ) - ? $outputDone // object fetched by hook - : $this->mParserOutput; - - # Adjust title for main page & pages with displaytitle - if ( $pOutput ) { - $this->adjustDisplayTitle( $pOutput ); - } - - # For the main page, overwrite the element with the con- - # tents of 'pagetitle-view-mainpage' instead of the default (if - # that's not empty). - # This message always exists because it is in the i18n files - if ( $this->getTitle()->isMainPage() ) { - $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage(); - if ( !$msg->isDisabled() ) { - $outputPage->setHTMLTitle( $msg->title( $this->getTitle() )->text() ); - } - } - - # Check for any __NOINDEX__ tags on the page using $pOutput - $policy = $this->getRobotPolicy( 'view', $pOutput ); - $outputPage->setIndexPolicy( $policy['index'] ); - $outputPage->setFollowPolicy( $policy['follow'] ); - - $this->showViewFooter(); - $this->mPage->doViewUpdates( $user, $oldid ); - - $outputPage->addModules( 'mediawiki.action.view.postEdit' ); - - wfProfileOut( __METHOD__ ); - } - - /** - * Adjust title for pages with displaytitle, -{T|}- or language conversion - * @param ParserOutput $pOutput - */ - public function adjustDisplayTitle( ParserOutput $pOutput ) { - # Adjust the title if it was set by displaytitle, -{T|}- or language conversion - $titleText = $pOutput->getTitleText(); - if ( strval( $titleText ) !== '' ) { - $this->getContext()->getOutput()->setPageTitle( $titleText ); - } - } - - /** - * Show a diff page according to current request variables. For use within - * Article::view() only, other callers should use the DifferenceEngine class. - * - * @todo Make protected - */ - public function showDiffPage() { - $request = $this->getContext()->getRequest(); - $user = $this->getContext()->getUser(); - $diff = $request->getVal( 'diff' ); - $rcid = $request->getVal( 'rcid' ); - $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) ); - $purge = $request->getVal( 'action' ) == 'purge'; - $unhide = $request->getInt( 'unhide' ) == 1; - $oldid = $this->getOldID(); - - $rev = $this->getRevisionFetched(); - - if ( !$rev ) { - $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' ) ); - $this->getContext()->getOutput()->addWikiMsg( 'difference-missing-revision', $oldid, 1 ); - return; - } - - $contentHandler = $rev->getContentHandler(); - $de = $contentHandler->createDifferenceEngine( - $this->getContext(), - $oldid, - $diff, - $rcid, - $purge, - $unhide - ); - - // DifferenceEngine directly fetched the revision: - $this->mRevIdFetched = $de->mNewid; - $de->showDiffPage( $diffOnly ); - - // Run view updates for the newer revision being diffed (and shown - // below the diff if not $diffOnly). - list( $old, $new ) = $de->mapDiffPrevNext( $oldid, $diff ); - // New can be false, convert it to 0 - this conveniently means the latest revision - $this->mPage->doViewUpdates( $user, (int)$new ); - } - - /** - * Show a page view for a page formatted as CSS or JavaScript. To be called by - * Article::view() only. - * - * This exists mostly to serve the deprecated ShowRawCssJs hook (used to customize these views). - * It has been replaced by the ContentGetParserOutput hook, which lets you do the same but with - * more flexibility. - * - * @param bool $showCacheHint Whether to show a message telling the user - * to clear the browser cache (default: true). - */ - protected function showCssOrJsPage( $showCacheHint = true ) { - $outputPage = $this->getContext()->getOutput(); - - if ( $showCacheHint ) { - $dir = $this->getContext()->getLanguage()->getDir(); - $lang = $this->getContext()->getLanguage()->getCode(); - - $outputPage->wrapWikiMsg( - "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>", - 'clearyourcache' - ); - } - - $this->fetchContentObject(); - - if ( $this->mContentObject ) { - // Give hooks a chance to customise the output - if ( ContentHandler::runLegacyHooks( - 'ShowRawCssJs', - array( $this->mContentObject, $this->getTitle(), $outputPage ) ) - ) { - // If no legacy hooks ran, display the content of the parser output, including RL modules, - // but excluding metadata like categories and language links - $po = $this->mContentObject->getParserOutput( $this->getTitle() ); - $outputPage->addParserOutputContent( $po ); - } - } - } - - /** - * Get the robot policy to be used for the current view - * @param string $action The action= GET parameter - * @param ParserOutput|null $pOutput - * @return array The policy that should be set - * @todo: actions other than 'view' - */ - public function getRobotPolicy( $action, $pOutput = null ) { - global $wgArticleRobotPolicies, $wgNamespaceRobotPolicies, $wgDefaultRobotPolicy; - - $ns = $this->getTitle()->getNamespace(); - - # Don't index user and user talk pages for blocked users (bug 11443) - if ( ( $ns == NS_USER || $ns == NS_USER_TALK ) && !$this->getTitle()->isSubpage() ) { - $specificTarget = null; - $vagueTarget = null; - $titleText = $this->getTitle()->getText(); - if ( IP::isValid( $titleText ) ) { - $vagueTarget = $titleText; - } else { - $specificTarget = $titleText; - } - if ( Block::newFromTarget( $specificTarget, $vagueTarget ) instanceof Block ) { - return array( - 'index' => 'noindex', - 'follow' => 'nofollow' - ); - } - } - - if ( $this->mPage->getID() === 0 || $this->getOldID() ) { - # Non-articles (special pages etc), and old revisions - return array( - 'index' => 'noindex', - 'follow' => 'nofollow' - ); - } elseif ( $this->getContext()->getOutput()->isPrintable() ) { - # Discourage indexing of printable versions, but encourage following - return array( - 'index' => 'noindex', - 'follow' => 'follow' - ); - } elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) { - # For ?curid=x urls, disallow indexing - return array( - 'index' => 'noindex', - 'follow' => 'follow' - ); - } - - # Otherwise, construct the policy based on the various config variables. - $policy = self::formatRobotPolicy( $wgDefaultRobotPolicy ); - - if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) { - # Honour customised robot policies for this namespace - $policy = array_merge( - $policy, - self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] ) - ); - } - if ( $this->getTitle()->canUseNoindex() && is_object( $pOutput ) && $pOutput->getIndexPolicy() ) { - # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates - # a final sanity check that we have really got the parser output. - $policy = array_merge( - $policy, - array( 'index' => $pOutput->getIndexPolicy() ) - ); - } - - if ( isset( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) ) { - # (bug 14900) site config can override user-defined __INDEX__ or __NOINDEX__ - $policy = array_merge( - $policy, - self::formatRobotPolicy( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) - ); - } - - return $policy; - } - - /** - * Converts a String robot policy into an associative array, to allow - * merging of several policies using array_merge(). - * @param array|string $policy Returns empty array on null/false/'', transparent - * to already-converted arrays, converts string. - * @return array 'index' => \<indexpolicy\>, 'follow' => \<followpolicy\> - */ - public static function formatRobotPolicy( $policy ) { - if ( is_array( $policy ) ) { - return $policy; - } elseif ( !$policy ) { - return array(); - } - - $policy = explode( ',', $policy ); - $policy = array_map( 'trim', $policy ); - - $arr = array(); - foreach ( $policy as $var ) { - if ( in_array( $var, array( 'index', 'noindex' ) ) ) { - $arr['index'] = $var; - } elseif ( in_array( $var, array( 'follow', 'nofollow' ) ) ) { - $arr['follow'] = $var; - } - } - - return $arr; - } - - /** - * If this request is a redirect view, send "redirected from" subtitle to - * the output. Returns true if the header was needed, false if this is not - * a redirect view. Handles both local and remote redirects. - * - * @return bool - */ - public function showRedirectedFromHeader() { - global $wgRedirectSources; - $outputPage = $this->getContext()->getOutput(); - - $rdfrom = $this->getContext()->getRequest()->getVal( 'rdfrom' ); - - if ( isset( $this->mRedirectedFrom ) ) { - // This is an internally redirected page view. - // We'll need a backlink to the source page for navigation. - if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { - $redir = Linker::linkKnown( - $this->mRedirectedFrom, - null, - array(), - array( 'redirect' => 'no' ) - ); - - $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); - - // Set the fragment if one was specified in the redirect - if ( $this->getTitle()->hasFragment() ) { - $outputPage->addJsConfigVars( 'wgRedirectToFragment', $this->getTitle()->getFragmentForURL() ); - $outputPage->addModules( 'mediawiki.action.view.redirectToFragment' ); - } - - // Add a <link rel="canonical"> tag - $outputPage->setCanonicalUrl( $this->getTitle()->getLocalURL() ); - - // Tell the output object that the user arrived at this article through a redirect - $outputPage->setRedirectedFrom( $this->mRedirectedFrom ); - - return true; - } - } elseif ( $rdfrom ) { - // This is an externally redirected view, from some other wiki. - // If it was reported from a trusted site, supply a backlink. - if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { - $redir = Linker::makeExternalLink( $rdfrom, $rdfrom ); - $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); - - return true; - } - } - - return false; - } - - /** - * Show a header specific to the namespace currently being viewed, like - * [[MediaWiki:Talkpagetext]]. For Article::view(). - */ - public function showNamespaceHeader() { - if ( $this->getTitle()->isTalkPage() ) { - if ( !wfMessage( 'talkpageheader' )->isDisabled() ) { - $this->getContext()->getOutput()->wrapWikiMsg( - "<div class=\"mw-talkpageheader\">\n$1\n</div>", - array( 'talkpageheader' ) - ); - } - } - } - - /** - * Show the footer section of an ordinary page view - */ - public function showViewFooter() { - # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page - if ( $this->getTitle()->getNamespace() == NS_USER_TALK - && IP::isValid( $this->getTitle()->getText() ) - ) { - $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' ); - } - - // Show a footer allowing the user to patrol the shown revision or page if possible - $patrolFooterShown = $this->showPatrolFooter(); - - wfRunHooks( 'ArticleViewFooter', array( $this, $patrolFooterShown ) ); - } - - /** - * If patrol is possible, output a patrol UI box. This is called from the - * footer section of ordinary page views. If patrol is not possible or not - * desired, does nothing. - * Side effect: When the patrol link is build, this method will call - * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax. - * - * @return bool - */ - public function showPatrolFooter() { - global $wgUseNPPatrol, $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI; - - $outputPage = $this->getContext()->getOutput(); - $user = $this->getContext()->getUser(); - $cache = wfGetMainCache(); - $rc = false; - - if ( !$this->getTitle()->quickUserCan( 'patrol', $user ) - || !( $wgUseRCPatrol || $wgUseNPPatrol ) - ) { - // Patrolling is disabled or the user isn't allowed to - return false; - } - - wfProfileIn( __METHOD__ ); - - // New page patrol: Get the timestamp of the oldest revison which - // the revision table holds for the given page. Then we look - // whether it's within the RC lifespan and if it is, we try - // to get the recentchanges row belonging to that entry - // (with rc_new = 1). - - // Check for cached results - if ( $cache->get( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ) ) ) { - wfProfileOut( __METHOD__ ); - return false; - } - - if ( $this->mRevision - && !RecentChange::isInRCLifespan( $this->mRevision->getTimestamp(), 21600 ) - ) { - // The current revision is already older than what could be in the RC table - // 6h tolerance because the RC might not be cleaned out regularly - wfProfileOut( __METHOD__ ); - return false; - } - - $dbr = wfGetDB( DB_SLAVE ); - $oldestRevisionTimestamp = $dbr->selectField( - 'revision', - 'MIN( rev_timestamp )', - array( 'rev_page' => $this->getTitle()->getArticleID() ), - __METHOD__ - ); - - if ( $oldestRevisionTimestamp - && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 ) - ) { - // 6h tolerance because the RC might not be cleaned out regularly - $rc = RecentChange::newFromConds( - array( - 'rc_new' => 1, - 'rc_timestamp' => $oldestRevisionTimestamp, - 'rc_namespace' => $this->getTitle()->getNamespace(), - 'rc_cur_id' => $this->getTitle()->getArticleID(), - 'rc_patrolled' => 0 - ), - __METHOD__, - array( 'USE INDEX' => 'new_name_timestamp' ) - ); - } - - if ( !$rc ) { - // No RC entry around - - // Cache the information we gathered above in case we can't patrol - // Don't cache in case we can patrol as this could change - $cache->set( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ), '1' ); - - wfProfileOut( __METHOD__ ); - return false; - } - - if ( $rc->getPerformer()->getName() == $user->getName() ) { - // Don't show a patrol link for own creations. If the user could - // patrol them, they already would be patrolled - wfProfileOut( __METHOD__ ); - return false; - } - - $rcid = $rc->getAttribute( 'rc_id' ); - - $token = $user->getEditToken( $rcid ); - - $outputPage->preventClickjacking(); - if ( $wgEnableAPI && $wgEnableWriteAPI && $user->isAllowed( 'writeapi' ) ) { - $outputPage->addModules( 'mediawiki.page.patrol.ajax' ); - } - - $link = Linker::linkKnown( - $this->getTitle(), - wfMessage( 'markaspatrolledtext' )->escaped(), - array(), - array( - 'action' => 'markpatrolled', - 'rcid' => $rcid, - 'token' => $token, - ) - ); - - $outputPage->addHTML( - "<div class='patrollink'>" . - wfMessage( 'markaspatrolledlink' )->rawParams( $link )->escaped() . - '</div>' - ); - - wfProfileOut( __METHOD__ ); - return true; - } - - /** - * Show the error text for a missing article. For articles in the MediaWiki - * namespace, show the default message text. To be called from Article::view(). - */ - public function showMissingArticle() { - global $wgSend404Code; - $outputPage = $this->getContext()->getOutput(); - // Whether the page is a root user page of an existing user (but not a subpage) - $validUserPage = false; - - # Show info in user (talk) namespace. Does the user exist? Is he blocked? - if ( $this->getTitle()->getNamespace() == NS_USER - || $this->getTitle()->getNamespace() == NS_USER_TALK - ) { - $parts = explode( '/', $this->getTitle()->getText() ); - $rootPart = $parts[0]; - $user = User::newFromName( $rootPart, false /* allow IP users*/ ); - $ip = User::isIP( $rootPart ); - - if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist - $outputPage->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", - array( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ) ); - } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked - LogEventsList::showLogExtract( - $outputPage, - 'block', - $user->getUserPage(), - '', - array( - 'lim' => 1, - 'showIfEmpty' => false, - 'msgKey' => array( - 'blocked-notice-logextract', - $user->getName() # Support GENDER in notice - ) - ) - ); - $validUserPage = !$this->getTitle()->isSubpage(); - } else { - $validUserPage = !$this->getTitle()->isSubpage(); - } - } - - wfRunHooks( 'ShowMissingArticle', array( $this ) ); - - // Give extensions a chance to hide their (unrelated) log entries - $logTypes = array( 'delete', 'move' ); - $conds = array( "log_action != 'revision'" ); - wfRunHooks( 'Article::MissingArticleConditions', array( &$conds, $logTypes ) ); - - # Show delete and move logs - LogEventsList::showLogExtract( $outputPage, $logTypes, $this->getTitle(), '', - array( 'lim' => 10, - 'conds' => $conds, - 'showIfEmpty' => false, - 'msgKey' => array( 'moveddeleted-notice' ) ) - ); - - if ( !$this->mPage->hasViewableContent() && $wgSend404Code && !$validUserPage ) { - // If there's no backing content, send a 404 Not Found - // for better machine handling of broken links. - $this->getContext()->getRequest()->response()->header( "HTTP/1.1 404 Not Found" ); - } - - if ( $validUserPage ) { - // Also apply the robot policy for nonexisting user pages (as those aren't served as 404) - $policy = $this->getRobotPolicy( 'view' ); - $outputPage->setIndexPolicy( $policy['index'] ); - $outputPage->setFollowPolicy( $policy['follow'] ); - } - - $hookResult = wfRunHooks( 'BeforeDisplayNoArticleText', array( $this ) ); - - if ( ! $hookResult ) { - return; - } - - # Show error message - $oldid = $this->getOldID(); - if ( $oldid ) { - $text = wfMessage( 'missing-revision', $oldid )->plain(); - } elseif ( $this->getTitle()->getNamespace() === NS_MEDIAWIKI ) { - // Use the default message text - $text = $this->getTitle()->getDefaultMessageText(); - } elseif ( $this->getTitle()->quickUserCan( 'create', $this->getContext()->getUser() ) - && $this->getTitle()->quickUserCan( 'edit', $this->getContext()->getUser() ) - ) { - $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; - $text = wfMessage( $message )->plain(); - } else { - $text = wfMessage( 'noarticletext-nopermission' )->plain(); - } - $text = "<div class='noarticletext'>\n$text\n</div>"; - - $outputPage->addWikiText( $text ); - } - - /** - * If the revision requested for view is deleted, check permissions. - * Send either an error message or a warning header to the output. - * - * @return bool true if the view is allowed, false if not. - */ - public function showDeletedRevisionHeader() { - if ( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - // Not deleted - return true; - } - - $outputPage = $this->getContext()->getOutput(); - $user = $this->getContext()->getUser(); - // If the user is not allowed to see it... - if ( !$this->mRevision->userCan( Revision::DELETED_TEXT, $user ) ) { - $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", - 'rev-deleted-text-permission' ); - - return false; - // If the user needs to confirm that they want to see it... - } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) { - # Give explanation and add a link to view the revision... - $oldid = intval( $this->getOldID() ); - $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" ); - $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? - 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide'; - $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", - array( $msg, $link ) ); - - return false; - // We are allowed to see... - } else { - $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? - 'rev-suppressed-text-view' : 'rev-deleted-text-view'; - $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg ); - - return true; - } - } - - /** - * Generate the navigation links when browsing through an article revisions - * It shows the information as: - * Revision as of \<date\>; view current revision - * \<- Previous version | Next Version -\> - * - * @param int $oldid Revision ID of this article revision - */ - public function setOldSubtitle( $oldid = 0 ) { - if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { - return; - } - - $unhide = $this->getContext()->getRequest()->getInt( 'unhide' ) == 1; - - # Cascade unhide param in links for easy deletion browsing - $extraParams = array(); - if ( $unhide ) { - $extraParams['unhide'] = 1; - } - - if ( $this->mRevision && $this->mRevision->getId() === $oldid ) { - $revision = $this->mRevision; - } else { - $revision = Revision::newFromId( $oldid ); - } - - $timestamp = $revision->getTimestamp(); - - $current = ( $oldid == $this->mPage->getLatest() ); - $language = $this->getContext()->getLanguage(); - $user = $this->getContext()->getUser(); - - $td = $language->userTimeAndDate( $timestamp, $user ); - $tddate = $language->userDate( $timestamp, $user ); - $tdtime = $language->userTime( $timestamp, $user ); - - # Show user links if allowed to see them. If hidden, then show them only if requested... - $userlinks = Linker::revUserTools( $revision, !$unhide ); - - $infomsg = $current && !wfMessage( 'revision-info-current' )->isDisabled() - ? 'revision-info-current' - : 'revision-info'; - - $outputPage = $this->getContext()->getOutput(); - $outputPage->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg, - $td )->rawParams( $userlinks )->params( $revision->getID(), $tddate, - $tdtime, $revision->getUser() )->rawParams( Linker::revComment( $revision, true, true ) )->parse() . "</div>" ); - - $lnk = $current - ? wfMessage( 'currentrevisionlink' )->escaped() - : Linker::linkKnown( - $this->getTitle(), - wfMessage( 'currentrevisionlink' )->escaped(), - array(), - $extraParams - ); - $curdiff = $current - ? wfMessage( 'diff' )->escaped() - : Linker::linkKnown( - $this->getTitle(), - wfMessage( 'diff' )->escaped(), - array(), - array( - 'diff' => 'cur', - 'oldid' => $oldid - ) + $extraParams - ); - $prev = $this->getTitle()->getPreviousRevisionID( $oldid ); - $prevlink = $prev - ? Linker::linkKnown( - $this->getTitle(), - wfMessage( 'previousrevision' )->escaped(), - array(), - array( - 'direction' => 'prev', - 'oldid' => $oldid - ) + $extraParams - ) - : wfMessage( 'previousrevision' )->escaped(); - $prevdiff = $prev - ? Linker::linkKnown( - $this->getTitle(), - wfMessage( 'diff' )->escaped(), - array(), - array( - 'diff' => 'prev', - 'oldid' => $oldid - ) + $extraParams - ) - : wfMessage( 'diff' )->escaped(); - $nextlink = $current - ? wfMessage( 'nextrevision' )->escaped() - : Linker::linkKnown( - $this->getTitle(), - wfMessage( 'nextrevision' )->escaped(), - array(), - array( - 'direction' => 'next', - 'oldid' => $oldid - ) + $extraParams - ); - $nextdiff = $current - ? wfMessage( 'diff' )->escaped() - : Linker::linkKnown( - $this->getTitle(), - wfMessage( 'diff' )->escaped(), - array(), - array( - 'diff' => 'next', - 'oldid' => $oldid - ) + $extraParams - ); - - $cdel = Linker::getRevDeleteLink( $user, $revision, $this->getTitle() ); - if ( $cdel !== '' ) { - $cdel .= ' '; - } - - $outputPage->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel . - wfMessage( 'revision-nav' )->rawParams( - $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff - )->escaped() . "</div>" ); - } - - /** - * Return the HTML for the top of a redirect page - * - * Chances are you should just be using the ParserOutput from - * WikitextContent::getParserOutput instead of calling this for redirects. - * - * @param Title|array $target Destination(s) to redirect - * @param bool $appendSubtitle [optional] - * @param bool $forceKnown Should the image be shown as a bluelink regardless of existence? - * @return string Containing HMTL with redirect link - */ - public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { - $lang = $this->getTitle()->getPageLanguage(); - if ( $appendSubtitle ) { - $out = $this->getContext()->getOutput(); - $out->addSubtitle( wfMessage( 'redirectpagesub' )->parse() ); - } - return static::getRedirectHeaderHtml( $lang, $target, $forceKnown ); - } - - /** - * Return the HTML for the top of a redirect page - * - * Chances are you should just be using the ParserOutput from - * WikitextContent::getParserOutput instead of calling this for redirects. - * - * @since 1.23 - * @param Language $lang - * @param Title|array $target Destination(s) to redirect - * @param bool $forceKnown Should the image be shown as a bluelink regardless of existence? - * @return string Containing HMTL with redirect link - */ - public static function getRedirectHeaderHtml( Language $lang, $target, $forceKnown = false ) { - global $wgStylePath; - - if ( !is_array( $target ) ) { - $target = array( $target ); - } - - $imageDir = $lang->getDir(); - - // the loop prepends the arrow image before the link, so the first case needs to be outside - - /** @var $title Title */ - $title = array_shift( $target ); - - if ( $forceKnown ) { - $link = Linker::linkKnown( $title, htmlspecialchars( $title->getFullText() ) ); - } else { - $link = Linker::link( $title, htmlspecialchars( $title->getFullText() ) ); - } - - $nextRedirect = $wgStylePath . '/common/images/nextredirect' . $imageDir . '.png'; - $alt = $lang->isRTL() ? '←' : '→'; - - // Automatically append redirect=no to each link, since most of them are - // redirect pages themselves. - /** @var Title $rt */ - foreach ( $target as $rt ) { - $link .= Html::element( 'img', array( 'src' => $nextRedirect, 'alt' => $alt ) ); - if ( $forceKnown ) { - $link .= Linker::linkKnown( - $rt, - htmlspecialchars( $rt->getFullText(), - array(), - array( 'redirect' => 'no' ) - ) - ); - } else { - $link .= Linker::link( - $rt, - htmlspecialchars( $rt->getFullText() ), - array(), - array( 'redirect' => 'no' ) - ); - } - } - - $imageUrl = $wgStylePath . '/common/images/redirect' . $imageDir . '.png'; - return '<div class="redirectMsg">' . - Html::element( 'img', array( 'src' => $imageUrl, 'alt' => '#REDIRECT' ) ) . - '<span class="redirectText">' . $link . '</span></div>'; - } - - /** - * Handle action=render - */ - public function render() { - $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' ); - $this->getContext()->getOutput()->setArticleBodyOnly( true ); - $this->getContext()->getOutput()->enableSectionEditLinks( false ); - $this->view(); - } - - /** - * action=protect handler - */ - public function protect() { - $form = new ProtectionForm( $this ); - $form->execute(); - } - - /** - * action=unprotect handler (alias) - */ - public function unprotect() { - $this->protect(); - } - - /** - * UI entry point for page deletion - */ - public function delete() { - # This code desperately needs to be totally rewritten - - $title = $this->getTitle(); - $user = $this->getContext()->getUser(); - - # Check permissions - $permission_errors = $title->getUserPermissionsErrors( 'delete', $user ); - if ( count( $permission_errors ) ) { - throw new PermissionsError( 'delete', $permission_errors ); - } - - # Read-only check... - if ( wfReadOnly() ) { - throw new ReadOnlyError; - } - - # Better double-check that it hasn't been deleted yet! - $this->mPage->loadPageData( 'fromdbmaster' ); - if ( !$this->mPage->exists() ) { - $deleteLogPage = new LogPage( 'delete' ); - $outputPage = $this->getContext()->getOutput(); - $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) ); - $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>", - array( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) ) - ); - $outputPage->addHTML( - Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) - ); - LogEventsList::showLogExtract( - $outputPage, - 'delete', - $title - ); - - return; - } - - $request = $this->getContext()->getRequest(); - $deleteReasonList = $request->getText( 'wpDeleteReasonList', 'other' ); - $deleteReason = $request->getText( 'wpReason' ); - - if ( $deleteReasonList == 'other' ) { - $reason = $deleteReason; - } elseif ( $deleteReason != '' ) { - // Entry from drop down menu + additional comment - $colonseparator = wfMessage( 'colon-separator' )->inContentLanguage()->text(); - $reason = $deleteReasonList . $colonseparator . $deleteReason; - } else { - $reason = $deleteReasonList; - } - - if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ), - array( 'delete', $this->getTitle()->getPrefixedText() ) ) - ) { - # Flag to hide all contents of the archived revisions - $suppress = $request->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' ); - - $this->doDelete( $reason, $suppress ); - - WatchAction::doWatchOrUnwatch( $request->getCheck( 'wpWatch' ), $title, $user ); - - return; - } - - // Generate deletion reason - $hasHistory = false; - if ( !$reason ) { - try { - $reason = $this->generateReason( $hasHistory ); - } catch ( MWException $e ) { - # if a page is horribly broken, we still want to be able to - # delete it. So be lenient about errors here. - wfDebug( "Error while building auto delete summary: $e" ); - $reason = ''; - } - } - - // If the page has a history, insert a warning - if ( $hasHistory ) { - $revisions = $this->mTitle->estimateRevisionCount(); - // @todo FIXME: i18n issue/patchwork message - $this->getContext()->getOutput()->addHTML( '<strong class="mw-delete-warning-revisions">' . - wfMessage( 'historywarning' )->numParams( $revisions )->parse() . - wfMessage( 'word-separator' )->plain() . Linker::linkKnown( $title, - wfMessage( 'history' )->escaped(), - array( 'rel' => 'archives' ), - array( 'action' => 'history' ) ) . - '</strong>' - ); - - if ( $this->mTitle->isBigDeletion() ) { - global $wgDeleteRevisionsLimit; - $this->getContext()->getOutput()->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", - array( - 'delete-warning-toobig', - $this->getContext()->getLanguage()->formatNum( $wgDeleteRevisionsLimit ) - ) - ); - } - } - - $this->confirmDelete( $reason ); - } - - /** - * Output deletion confirmation dialog - * @todo FIXME: Move to another file? - * @param string $reason Prefilled reason - */ - public function confirmDelete( $reason ) { - wfDebug( "Article::confirmDelete\n" ); - - $outputPage = $this->getContext()->getOutput(); - $outputPage->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) ); - $outputPage->addBacklinkSubtitle( $this->getTitle() ); - $outputPage->setRobotPolicy( 'noindex,nofollow' ); - $backlinkCache = $this->getTitle()->getBacklinkCache(); - if ( $backlinkCache->hasLinks( 'pagelinks' ) || $backlinkCache->hasLinks( 'templatelinks' ) ) { - $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", - 'deleting-backlinks-warning' ); - } - $outputPage->addWikiMsg( 'confirmdeletetext' ); - - wfRunHooks( 'ArticleConfirmDelete', array( $this, $outputPage, &$reason ) ); - - $user = $this->getContext()->getUser(); - - if ( $user->isAllowed( 'suppressrevision' ) ) { - $suppress = "<tr id=\"wpDeleteSuppressRow\"> - <td></td> - <td class='mw-input'><strong>" . - Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(), - 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) . - "</strong></td> - </tr>"; - } else { - $suppress = ''; - } - $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $user->isWatched( $this->getTitle() ); - - $form = Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $this->getTitle()->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . - Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) . - Xml::tags( 'legend', null, wfMessage( 'delete-legend' )->escaped() ) . - Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) . - "<tr id=\"wpDeleteReasonListRow\"> - <td class='mw-label'>" . - Xml::label( wfMessage( 'deletecomment' )->text(), 'wpDeleteReasonList' ) . - "</td> - <td class='mw-input'>" . - Xml::listDropDown( - 'wpDeleteReasonList', - wfMessage( 'deletereason-dropdown' )->inContentLanguage()->text(), - wfMessage( 'deletereasonotherlist' )->inContentLanguage()->text(), - '', - 'wpReasonDropDown', - 1 - ) . - "</td> - </tr> - <tr id=\"wpDeleteReasonRow\"> - <td class='mw-label'>" . - Xml::label( wfMessage( 'deleteotherreason' )->text(), 'wpReason' ) . - "</td> - <td class='mw-input'>" . - Html::input( 'wpReason', $reason, 'text', array( - 'size' => '60', - 'maxlength' => '255', - 'tabindex' => '2', - 'id' => 'wpReason', - 'autofocus' - ) ) . - "</td> - </tr>"; - - # Disallow watching if user is not logged in - if ( $user->isLoggedIn() ) { - $form .= " - <tr> - <td></td> - <td class='mw-input'>" . - Xml::checkLabel( wfMessage( 'watchthis' )->text(), - 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) . - "</td> - </tr>"; - } - - $form .= " - $suppress - <tr> - <td></td> - <td class='mw-submit'>" . - Xml::submitButton( wfMessage( 'deletepage' )->text(), - array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) . - "</td> - </tr>" . - Xml::closeElement( 'table' ) . - Xml::closeElement( 'fieldset' ) . - Html::hidden( - 'wpEditToken', - $user->getEditToken( array( 'delete', $this->getTitle()->getPrefixedText() ) ) - ) . - Xml::closeElement( 'form' ); - - if ( $user->isAllowed( 'editinterface' ) ) { - $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ); - $link = Linker::link( - $title, - wfMessage( 'delete-edit-reasonlist' )->escaped(), - array(), - array( 'action' => 'edit' ) - ); - $form .= '<p class="mw-delete-editreasons">' . $link . '</p>'; - } - - $outputPage->addHTML( $form ); - - $deleteLogPage = new LogPage( 'delete' ); - $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) ); - LogEventsList::showLogExtract( $outputPage, 'delete', - $this->getTitle() - ); - } - - /** - * Perform a deletion and output success or failure messages - * @param string $reason - * @param bool $suppress - */ - public function doDelete( $reason, $suppress = false ) { - $error = ''; - $outputPage = $this->getContext()->getOutput(); - $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error ); - - if ( $status->isGood() ) { - $deleted = $this->getTitle()->getPrefixedText(); - - $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) ); - $outputPage->setRobotPolicy( 'noindex,nofollow' ); - - $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]'; - - $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink ); - $outputPage->returnToMain( false ); - } else { - $outputPage->setPageTitle( - wfMessage( 'cannotdelete-title', - $this->getTitle()->getPrefixedText() ) - ); - - if ( $error == '' ) { - $outputPage->addWikiText( - "<div class=\"error mw-error-cannotdelete\">\n" . $status->getWikiText() . "\n</div>" - ); - $deleteLogPage = new LogPage( 'delete' ); - $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) ); - - LogEventsList::showLogExtract( - $outputPage, - 'delete', - $this->getTitle() - ); - } else { - $outputPage->addHTML( $error ); - } - } - } - - /* Caching functions */ - - /** - * checkLastModified returns true if it has taken care of all - * output to the client that is necessary for this request. - * (that is, it has sent a cached version of the page) - * - * @return bool true if cached version send, false otherwise - */ - protected function tryFileCache() { - static $called = false; - - if ( $called ) { - wfDebug( "Article::tryFileCache(): called twice!?\n" ); - return false; - } - - $called = true; - if ( $this->isFileCacheable() ) { - $cache = HTMLFileCache::newFromTitle( $this->getTitle(), 'view' ); - if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) { - wfDebug( "Article::tryFileCache(): about to load file\n" ); - $cache->loadFromFileCache( $this->getContext() ); - return true; - } else { - wfDebug( "Article::tryFileCache(): starting buffer\n" ); - ob_start( array( &$cache, 'saveToFileCache' ) ); - } - } else { - wfDebug( "Article::tryFileCache(): not cacheable\n" ); - } - - return false; - } - - /** - * Check if the page can be cached - * @return bool - */ - public function isFileCacheable() { - $cacheable = false; - - if ( HTMLFileCache::useFileCache( $this->getContext() ) ) { - $cacheable = $this->mPage->getID() - && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect(); - // Extension may have reason to disable file caching on some pages. - if ( $cacheable ) { - $cacheable = wfRunHooks( 'IsFileCacheable', array( &$this ) ); - } - } - - return $cacheable; - } - - /**#@-*/ - - /** - * Lightweight method to get the parser output for a page, checking the parser cache - * and so on. Doesn't consider most of the stuff that WikiPage::view is forced to - * consider, so it's not appropriate to use there. - * - * @since 1.16 (r52326) for LiquidThreads - * - * @param int|null $oldid Revision ID or null - * @param User $user The relevant user - * @return ParserOutput|bool ParserOutput or false if the given revision ID is not found - */ - public function getParserOutput( $oldid = null, User $user = null ) { - //XXX: bypasses mParserOptions and thus setParserOptions() - - if ( $user === null ) { - $parserOptions = $this->getParserOptions(); - } else { - $parserOptions = $this->mPage->makeParserOptions( $user ); - } - - return $this->mPage->getParserOutput( $parserOptions, $oldid ); - } - - /** - * Override the ParserOptions used to render the primary article wikitext. - * - * @param ParserOptions $options - * @throws MWException if the parser options where already initialized. - */ - public function setParserOptions( ParserOptions $options ) { - if ( $this->mParserOptions ) { - throw new MWException( "can't change parser options after they have already been set" ); - } - - // clone, so if $options is modified later, it doesn't confuse the parser cache. - $this->mParserOptions = clone $options; - } - - /** - * Get parser options suitable for rendering the primary article wikitext - * @return ParserOptions - */ - public function getParserOptions() { - if ( !$this->mParserOptions ) { - $this->mParserOptions = $this->mPage->makeParserOptions( $this->getContext() ); - } - // Clone to allow modifications of the return value without affecting cache - return clone $this->mParserOptions; - } - - /** - * Sets the context this Article is executed in - * - * @param IContextSource $context - * @since 1.18 - */ - public function setContext( $context ) { - $this->mContext = $context; - } - - /** - * Gets the context this Article is executed in - * - * @return IContextSource - * @since 1.18 - */ - public function getContext() { - if ( $this->mContext instanceof IContextSource ) { - return $this->mContext; - } else { - wfDebug( __METHOD__ . " called and \$mContext is null. " . - "Return RequestContext::getMain(); for sanity\n" ); - return RequestContext::getMain(); - } - } - - /** - * Use PHP's magic __get handler to handle accessing of - * raw WikiPage fields for backwards compatibility. - * - * @param string $fname Field name - */ - public function __get( $fname ) { - if ( property_exists( $this->mPage, $fname ) ) { - #wfWarn( "Access to raw $fname field " . __CLASS__ ); - return $this->mPage->$fname; - } - trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE ); - } - - /** - * Use PHP's magic __set handler to handle setting of - * raw WikiPage fields for backwards compatibility. - * - * @param string $fname Field name - * @param mixed $fvalue New value - */ - public function __set( $fname, $fvalue ) { - if ( property_exists( $this->mPage, $fname ) ) { - #wfWarn( "Access to raw $fname field of " . __CLASS__ ); - $this->mPage->$fname = $fvalue; - // Note: extensions may want to toss on new fields - } elseif ( !in_array( $fname, array( 'mContext', 'mPage' ) ) ) { - $this->mPage->$fname = $fvalue; - } else { - trigger_error( 'Inaccessible property via __set(): ' . $fname, E_USER_NOTICE ); - } - } - - /** - * Use PHP's magic __call handler to transform instance calls to - * WikiPage functions for backwards compatibility. - * - * @param string $fname Name of called method - * @param array $args Arguments to the method - * @return mixed - */ - public function __call( $fname, $args ) { - if ( is_callable( array( $this->mPage, $fname ) ) ) { - #wfWarn( "Call to " . __CLASS__ . "::$fname; please use WikiPage instead" ); - return call_user_func_array( array( $this->mPage, $fname ), $args ); - } - trigger_error( 'Inaccessible function via __call(): ' . $fname, E_USER_ERROR ); - } - - // ****** B/C functions to work-around PHP silliness with __call and references ****** // - - /** - * @param array $limit - * @param array $expiry - * @param bool $cascade - * @param string $reason - * @param User $user - * @return Status - */ - public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, - $reason, User $user - ) { - return $this->mPage->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user ); - } - - /** - * @param array $limit - * @param string $reason - * @param int $cascade - * @param array $expiry - * @return bool - */ - public function updateRestrictions( $limit = array(), $reason = '', - &$cascade = 0, $expiry = array() - ) { - return $this->mPage->doUpdateRestrictions( - $limit, - $expiry, - $cascade, - $reason, - $this->getContext()->getUser() - ); - } - - /** - * @param string $reason - * @param bool $suppress - * @param int $id - * @param bool $commit - * @param string $error - * @return bool - */ - public function doDeleteArticle( $reason, $suppress = false, $id = 0, - $commit = true, &$error = '' - ) { - return $this->mPage->doDeleteArticle( $reason, $suppress, $id, $commit, $error ); - } - - /** - * @param string $fromP - * @param string $summary - * @param string $token - * @param bool $bot - * @param array $resultDetails - * @param User|null $user - * @return array - */ - public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user = null ) { - $user = is_null( $user ) ? $this->getContext()->getUser() : $user; - return $this->mPage->doRollback( $fromP, $summary, $token, $bot, $resultDetails, $user ); - } - - /** - * @param string $fromP - * @param string $summary - * @param bool $bot - * @param array $resultDetails - * @param User|null $guser - * @return array - */ - public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser = null ) { - $guser = is_null( $guser ) ? $this->getContext()->getUser() : $guser; - return $this->mPage->commitRollback( $fromP, $summary, $bot, $resultDetails, $guser ); - } - - /** - * @param bool $hasHistory - * @return mixed - */ - public function generateReason( &$hasHistory ) { - $title = $this->mPage->getTitle(); - $handler = ContentHandler::getForTitle( $title ); - return $handler->getAutoDeleteReason( $title, $hasHistory ); - } - - // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** // - - /** - * @return array - */ - public static function selectFields() { - return WikiPage::selectFields(); - } - - /** - * @param Title $title - */ - public static function onArticleCreate( $title ) { - WikiPage::onArticleCreate( $title ); - } - - /** - * @param Title $title - */ - public static function onArticleDelete( $title ) { - WikiPage::onArticleDelete( $title ); - } - - /** - * @param Title $title - */ - public static function onArticleEdit( $title ) { - WikiPage::onArticleEdit( $title ); - } - - /** - * @param string $oldtext - * @param string $newtext - * @param int $flags - * @return string - * @deprecated since 1.21, use ContentHandler::getAutosummary() instead - */ - public static function getAutosummary( $oldtext, $newtext, $flags ) { - return WikiPage::getAutosummary( $oldtext, $newtext, $flags ); - } - // ****** -} diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 54425076e2..94264aedde 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -32,7 +32,6 @@ $wgAutoloadLocalClasses = array( 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', 'AjaxResponse' => 'includes/AjaxResponse.php', 'AlphabeticPager' => 'includes/Pager.php', - 'Article' => 'includes/Article.php', 'AtomFeed' => 'includes/Feed.php', 'AuthPlugin' => 'includes/AuthPlugin.php', 'AuthPluginUser' => 'includes/AuthPlugin.php', @@ -42,7 +41,6 @@ $wgAutoloadLocalClasses = array( 'CacheHelper' => 'includes/CacheHelper.php', 'Category' => 'includes/Category.php', 'Categoryfinder' => 'includes/Categoryfinder.php', - 'CategoryPage' => 'includes/CategoryPage.php', 'CategoryViewer' => 'includes/CategoryViewer.php', 'ChangesFeed' => 'includes/ChangesFeed.php', 'ChangeTags' => 'includes/ChangeTags.php', @@ -112,9 +110,6 @@ $wgAutoloadLocalClasses = array( 'ICacheHelper' => 'includes/CacheHelper.php', 'IcuCollation' => 'includes/Collation.php', 'IdentityCollation' => 'includes/Collation.php', - 'ImageHistoryList' => 'includes/ImagePage.php', - 'ImageHistoryPseudoPager' => 'includes/ImagePage.php', - 'ImagePage' => 'includes/ImagePage.php', 'ImageQueryPage' => 'includes/ImageQueryPage.php', 'ImportStreamSource' => 'includes/Import.php', 'ImportStringSource' => 'includes/Import.php', @@ -145,7 +140,6 @@ $wgAutoloadLocalClasses = array( 'MWInit' => 'includes/Init.php', 'MWNamespace' => 'includes/Namespace.php', 'OutputPage' => 'includes/OutputPage.php', - 'Page' => 'includes/WikiPage.php', 'PageQueryPage' => 'includes/PageQueryPage.php', 'Pager' => 'includes/Pager.php', 'PasswordError' => 'includes/User.php', @@ -157,7 +151,7 @@ $wgAutoloadLocalClasses = array( 'PoolCounterRedis' => 'includes/poolcounter/PoolCounterRedis.php', 'PoolCounterWork' => 'includes/poolcounter/PoolCounterWork.php', 'PoolCounterWorkViaCallback' => 'includes/poolcounter/PoolCounterWork.php', - 'PoolWorkArticleView' => 'includes/WikiPage.php', + 'PoolWorkArticleView' => 'includes/poolcounter/PoolWorkArticleView.php', 'Preferences' => 'includes/Preferences.php', 'PreferencesForm' => 'includes/Preferences.php', 'PrefixSearch' => 'includes/PrefixSearch.php', @@ -207,11 +201,8 @@ $wgAutoloadLocalClasses = array( 'WebRequest' => 'includes/WebRequest.php', 'WebRequestUpload' => 'includes/WebRequest.php', 'WebResponse' => 'includes/WebResponse.php', - 'WikiCategoryPage' => 'includes/WikiCategoryPage.php', 'WikiExporter' => 'includes/Export.php', - 'WikiFilePage' => 'includes/WikiFilePage.php', 'WikiImporter' => 'includes/Import.php', - 'WikiPage' => 'includes/WikiPage.php', 'WikiRevision' => 'includes/Import.php', 'WikiMap' => 'includes/WikiMap.php', 'WikiReference' => 'includes/WikiMap.php', @@ -796,6 +787,17 @@ $wgAutoloadLocalClasses = array( 'WinCacheBagOStuff' => 'includes/objectcache/WinCacheBagOStuff.php', 'XCacheBagOStuff' => 'includes/objectcache/XCacheBagOStuff.php', + # includes/page + 'Article' => 'includes/page/Article.php', + 'CategoryPage' => 'includes/page/CategoryPage.php', + 'ImageHistoryList' => 'includes/page/ImagePage.php', + 'ImageHistoryPseudoPager' => 'includes/page/ImagePage.php', + 'ImagePage' => 'includes/page/ImagePage.php', + 'Page' => 'includes/page/WikiPage.php', + 'WikiCategoryPage' => 'includes/page/WikiCategoryPage.php', + 'WikiFilePage' => 'includes/page/WikiFilePage.php', + 'WikiPage' => 'includes/page/WikiPage.php', + # includes/parser 'CacheTime' => 'includes/parser/CacheTime.php', 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php', diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php deleted file mode 100644 index 9abc6a89b0..0000000000 --- a/includes/CategoryPage.php +++ /dev/null @@ -1,118 +0,0 @@ -<?php -/** - * Special handling for category description pages. - * Modelled after ImagePage.php. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Special handling for category description pages, showing pages, - * subcategories and file that belong to the category - */ -class CategoryPage extends Article { - # Subclasses can change this to override the viewer class. - protected $mCategoryViewerClass = 'CategoryViewer'; - - /** - * @param Title $title - * @return WikiCategoryPage - */ - protected function newPage( Title $title ) { - // Overload mPage with a category-specific page - return new WikiCategoryPage( $title ); - } - - /** - * Constructor from a page id - * @param int $id Article ID to load - * @return CategoryPage|null - */ - public static function newFromID( $id ) { - $t = Title::newFromID( $id ); - # @todo FIXME: Doesn't inherit right - return $t == null ? null : new self( $t ); - # return $t == null ? null : new static( $t ); // PHP 5.3 - } - - function view() { - $request = $this->getContext()->getRequest(); - $diff = $request->getVal( 'diff' ); - $diffOnly = $request->getBool( 'diffonly', - $this->getContext()->getUser()->getOption( 'diffonly' ) ); - - if ( $diff !== null && $diffOnly ) { - parent::view(); - return; - } - - if ( !wfRunHooks( 'CategoryPageView', array( &$this ) ) ) { - return; - } - - $title = $this->getTitle(); - if ( NS_CATEGORY == $title->getNamespace() ) { - $this->openShowCategory(); - } - - parent::view(); - - if ( NS_CATEGORY == $title->getNamespace() ) { - $this->closeShowCategory(); - } - } - - function openShowCategory() { - # For overloading - } - - function closeShowCategory() { - // Use these as defaults for back compat --catrope - $request = $this->getContext()->getRequest(); - $oldFrom = $request->getVal( 'from' ); - $oldUntil = $request->getVal( 'until' ); - - $reqArray = $request->getValues(); - - $from = $until = array(); - foreach ( array( 'page', 'subcat', 'file' ) as $type ) { - $from[$type] = $request->getVal( "{$type}from", $oldFrom ); - $until[$type] = $request->getVal( "{$type}until", $oldUntil ); - - // Do not want old-style from/until propagating in nav links. - if ( !isset( $reqArray["{$type}from"] ) && isset( $reqArray["from"] ) ) { - $reqArray["{$type}from"] = $reqArray["from"]; - } - if ( !isset( $reqArray["{$type}to"] ) && isset( $reqArray["to"] ) ) { - $reqArray["{$type}to"] = $reqArray["to"]; - } - } - - unset( $reqArray["from"] ); - unset( $reqArray["to"] ); - - $viewer = new $this->mCategoryViewerClass( - $this->getContext()->getTitle(), - $this->getContext(), - $from, - $until, - $reqArray - ); - $this->getContext()->getOutput()->addHTML( $viewer->getHTML() ); - } -} diff --git a/includes/ImagePage.php b/includes/ImagePage.php deleted file mode 100644 index 60db2025c2..0000000000 --- a/includes/ImagePage.php +++ /dev/null @@ -1,1567 +0,0 @@ -<?php -/** - * Special handling for file description pages. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Class for viewing MediaWiki file description pages - * - * @ingroup Media - */ -class ImagePage extends Article { - /** @var File */ - private $displayImg; - - /** @var FileRepo */ - private $repo; - - /** @var bool */ - private $fileLoaded; - - /** @var bool */ - protected $mExtraDescription = false; - - /** - * @param Title $title - * @return WikiFilePage - */ - protected function newPage( Title $title ) { - // Overload mPage with a file-specific page - return new WikiFilePage( $title ); - } - - /** - * Constructor from a page id - * @param int $id Article ID to load - * @return ImagePage|null - */ - public static function newFromID( $id ) { - $t = Title::newFromID( $id ); - # @todo FIXME: Doesn't inherit right - return $t == null ? null : new self( $t ); - # return $t == null ? null : new static( $t ); // PHP 5.3 - } - - /** - * @param File $file - * @return void - */ - public function setFile( $file ) { - $this->mPage->setFile( $file ); - $this->displayImg = $file; - $this->fileLoaded = true; - } - - protected function loadFile() { - if ( $this->fileLoaded ) { - return; - } - $this->fileLoaded = true; - - $this->displayImg = $img = false; - wfRunHooks( 'ImagePageFindFile', array( $this, &$img, &$this->displayImg ) ); - if ( !$img ) { // not set by hook? - $img = wfFindFile( $this->getTitle() ); - if ( !$img ) { - $img = wfLocalFile( $this->getTitle() ); - } - } - $this->mPage->setFile( $img ); - if ( !$this->displayImg ) { // not set by hook? - $this->displayImg = $img; - } - $this->repo = $img->getRepo(); - } - - /** - * Handler for action=render - * Include body text only; none of the image extras - */ - public function render() { - $this->getContext()->getOutput()->setArticleBodyOnly( true ); - parent::view(); - } - - public function view() { - global $wgShowEXIF; - - $out = $this->getContext()->getOutput(); - $request = $this->getContext()->getRequest(); - $diff = $request->getVal( 'diff' ); - $diffOnly = $request->getBool( - 'diffonly', - $this->getContext()->getUser()->getOption( 'diffonly' ) - ); - - if ( $this->getTitle()->getNamespace() != NS_FILE || ( $diff !== null && $diffOnly ) ) { - parent::view(); - return; - } - - $this->loadFile(); - - if ( $this->getTitle()->getNamespace() == NS_FILE && $this->mPage->getFile()->getRedirected() ) { - if ( $this->getTitle()->getDBkey() == $this->mPage->getFile()->getName() || $diff !== null ) { - // mTitle is the same as the redirect target so ask Article - // to perform the redirect for us. - $request->setVal( 'diffonly', 'true' ); - parent::view(); - return; - } else { - // mTitle is not the same as the redirect target so it is - // probably the redirect page itself. Fake the redirect symbol - $out->setPageTitle( $this->getTitle()->getPrefixedText() ); - $out->addHTML( $this->viewRedirect( - Title::makeTitle( NS_FILE, $this->mPage->getFile()->getName() ), - /* $appendSubtitle */ true, - /* $forceKnown */ true ) - ); - $this->mPage->doViewUpdates( $this->getContext()->getUser(), $this->getOldID() ); - return; - } - } - - if ( $wgShowEXIF && $this->displayImg->exists() ) { - // @todo FIXME: Bad interface, see note on MediaHandler::formatMetadata(). - $formattedMetadata = $this->displayImg->formatMetadata(); - $showmeta = $formattedMetadata !== false; - } else { - $showmeta = false; - } - - if ( !$diff && $this->displayImg->exists() ) { - $out->addHTML( $this->showTOC( $showmeta ) ); - } - - if ( !$diff ) { - $this->openShowImage(); - } - - # No need to display noarticletext, we use our own message, output in openShowImage() - if ( $this->mPage->getID() ) { - # NS_FILE is in the user language, but this section (the actual wikitext) - # should be in page content language - $pageLang = $this->getTitle()->getPageViewLanguage(); - $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content', - 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(), - 'class' => 'mw-content-' . $pageLang->getDir() ) ) ); - - parent::view(); - - $out->addHTML( Xml::closeElement( 'div' ) ); - } else { - # Just need to set the right headers - $out->setArticleFlag( true ); - $out->setPageTitle( $this->getTitle()->getPrefixedText() ); - $this->mPage->doViewUpdates( $this->getContext()->getUser(), $this->getOldID() ); - } - - # Show shared description, if needed - if ( $this->mExtraDescription ) { - $fol = wfMessage( 'shareddescriptionfollows' ); - if ( !$fol->isDisabled() ) { - $out->addWikiText( $fol->plain() ); - } - $out->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" ); - } - - $this->closeShowImage(); - $this->imageHistory(); - // TODO: Cleanup the following - - $out->addHTML( Xml::element( 'h2', - array( 'id' => 'filelinks' ), - wfMessage( 'imagelinks' )->text() ) . "\n" ); - $this->imageDupes(); - # @todo FIXME: For some freaky reason, we can't redirect to foreign images. - # Yet we return metadata about the target. Definitely an issue in the FileRepo - $this->imageLinks(); - - # Allow extensions to add something after the image links - $html = ''; - wfRunHooks( 'ImagePageAfterImageLinks', array( $this, &$html ) ); - if ( $html ) { - $out->addHTML( $html ); - } - - if ( $showmeta ) { - $out->addHTML( Xml::element( - 'h2', - array( 'id' => 'metadata' ), - wfMessage( 'metadata' )->text() ) . "\n" ); - $out->addWikiText( $this->makeMetadataTable( $formattedMetadata ) ); - $out->addModules( array( 'mediawiki.action.view.metadata' ) ); - } - - // Add remote Filepage.css - if ( !$this->repo->isLocal() ) { - $css = $this->repo->getDescriptionStylesheetUrl(); - if ( $css ) { - $out->addStyle( $css ); - } - } - // always show the local local Filepage.css, bug 29277 - $out->addModuleStyles( 'filepage' ); - } - - /** - * @return File - */ - public function getDisplayedFile() { - $this->loadFile(); - return $this->displayImg; - } - - /** - * Create the TOC - * - * @param bool $metadata Whether or not to show the metadata link - * @return string - */ - protected function showTOC( $metadata ) { - $r = array( - '<li><a href="#file">' . wfMessage( 'file-anchor-link' )->escaped() . '</a></li>', - '<li><a href="#filehistory">' . wfMessage( 'filehist' )->escaped() . '</a></li>', - '<li><a href="#filelinks">' . wfMessage( 'imagelinks' )->escaped() . '</a></li>', - ); - if ( $metadata ) { - $r[] = '<li><a href="#metadata">' . wfMessage( 'metadata' )->escaped() . '</a></li>'; - } - - wfRunHooks( 'ImagePageShowTOC', array( $this, &$r ) ); - - return '<ul id="filetoc">' . implode( "\n", $r ) . '</ul>'; - } - - /** - * Make a table with metadata to be shown in the output page. - * - * @todo FIXME: Bad interface, see note on MediaHandler::formatMetadata(). - * - * @param array $metadata The array containing the Exif data - * @return string The metadata table. This is treated as Wikitext (!) - */ - protected function makeMetadataTable( $metadata ) { - $r = "<div class=\"mw-imagepage-section-metadata\">"; - $r .= wfMessage( 'metadata-help' )->plain(); - $r .= "<table id=\"mw_metadata\" class=\"mw_metadata\">\n"; - foreach ( $metadata as $type => $stuff ) { - foreach ( $stuff as $v ) { - # @todo FIXME: Why is this using escapeId for a class?! - $class = Sanitizer::escapeId( $v['id'] ); - if ( $type == 'collapsed' ) { - // Handled by mediawiki.action.view.metadata module - // and skins/common/shared.css. - $class .= ' collapsable'; - } - $r .= "<tr class=\"$class\">\n"; - $r .= "<th>{$v['name']}</th>\n"; - $r .= "<td>{$v['value']}</td>\n</tr>"; - } - } - $r .= "</table>\n</div>\n"; - return $r; - } - - /** - * Overloading Article's getContentObject method. - * - * Omit noarticletext if sharedupload; text will be fetched from the - * shared upload server if possible. - * @return string - */ - public function getContentObject() { - $this->loadFile(); - if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) { - return null; - } - return parent::getContentObject(); - } - - protected function openShowImage() { - global $wgImageLimits, $wgEnableUploads, $wgSend404Code; - - $this->loadFile(); - $out = $this->getContext()->getOutput(); - $user = $this->getContext()->getUser(); - $lang = $this->getContext()->getLanguage(); - $dirmark = $lang->getDirMarkEntity(); - $request = $this->getContext()->getRequest(); - - $max = $this->getImageLimitsFromOption( $user, 'imagesize' ); - $maxWidth = $max[0]; - $maxHeight = $max[1]; - - if ( $this->displayImg->exists() ) { - # image - $page = $request->getIntOrNull( 'page' ); - if ( is_null( $page ) ) { - $params = array(); - $page = 1; - } else { - $params = array( 'page' => $page ); - } - - $renderLang = $request->getVal( 'lang' ); - if ( !is_null( $renderLang ) ) { - $handler = $this->displayImg->getHandler(); - if ( $handler && $handler->validateParam( 'lang', $renderLang ) ) { - $params['lang'] = $renderLang; - } else { - $renderLang = null; - } - } - - $width_orig = $this->displayImg->getWidth( $page ); - $width = $width_orig; - $height_orig = $this->displayImg->getHeight( $page ); - $height = $height_orig; - - $filename = wfEscapeWikiText( $this->displayImg->getName() ); - $linktext = $filename; - - wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$out ) ); - - if ( $this->displayImg->allowInlineDisplay() ) { - # image - - # "Download high res version" link below the image - # $msgsize = wfMessage( 'file-info-size', $width_orig, $height_orig, - # Linker::formatSize( $this->displayImg->getSize() ), $mime )->escaped(); - # We'll show a thumbnail of this image - if ( $width > $maxWidth || $height > $maxHeight ) { - # Calculate the thumbnail size. - # First case, the limiting factor is the width, not the height. - /** @todo // FIXME: Possible division by 0. bug 36911 */ - if ( $width / $height >= $maxWidth / $maxHeight ) { - /** @todo // FIXME: Possible division by 0. bug 36911 */ - $height = round( $height * $maxWidth / $width ); - $width = $maxWidth; - # Note that $height <= $maxHeight now. - } else { - /** @todo // FIXME: Possible division by 0. bug 36911 */ - $newwidth = floor( $width * $maxHeight / $height ); - /** @todo // FIXME: Possible division by 0. bug 36911 */ - $height = round( $height * $newwidth / $width ); - $width = $newwidth; - # Note that $height <= $maxHeight now, but might not be identical - # because of rounding. - } - $linktext = wfMessage( 'show-big-image' )->escaped(); - if ( $this->displayImg->getRepo()->canTransformVia404() ) { - $thumbSizes = $wgImageLimits; - // Also include the full sized resolution in the list, so - // that users know they can get it. This will link to the - // original file asset if mustRender() === false. In the case - // that we mustRender, some users have indicated that they would - // find it useful to have the full size image in the rendered - // image format. - $thumbSizes[] = array( $width_orig, $height_orig ); - } else { - # Creating thumb links triggers thumbnail generation. - # Just generate the thumb for the current users prefs. - $thumbSizes = array( $this->getImageLimitsFromOption( $user, 'thumbsize' ) ); - if ( !$this->displayImg->mustRender() ) { - // We can safely include a link to the "full-size" preview, - // without actually rendering. - $thumbSizes[] = array( $width_orig, $height_orig ); - } - } - # Generate thumbnails or thumbnail links as needed... - $otherSizes = array(); - foreach ( $thumbSizes as $size ) { - // We include a thumbnail size in the list, if it is - // less than or equal to the original size of the image - // asset ($width_orig/$height_orig). We also exclude - // the current thumbnail's size ($width/$height) - // since that is added to the message separately, so - // it can be denoted as the current size being shown. - if ( $size[0] <= $width_orig && $size[1] <= $height_orig - && $size[0] != $width && $size[1] != $height - ) { - $sizeLink = $this->makeSizeLink( $params, $size[0], $size[1] ); - if ( $sizeLink ) { - $otherSizes[] = $sizeLink; - } - } - } - $otherSizes = array_unique( $otherSizes ); - $msgsmall = ''; - $sizeLinkBigImagePreview = $this->makeSizeLink( $params, $width, $height ); - if ( $sizeLinkBigImagePreview ) { - $msgsmall .= wfMessage( 'show-big-image-preview' )-> - rawParams( $sizeLinkBigImagePreview )-> - parse(); - } - if ( count( $otherSizes ) ) { - $msgsmall .= ' ' . - Html::rawElement( 'span', array( 'class' => 'mw-filepage-other-resolutions' ), - wfMessage( 'show-big-image-other' )->rawParams( $lang->pipeList( $otherSizes ) )-> - params( count( $otherSizes ) )->parse() - ); - } - } elseif ( $width == 0 && $height == 0 ) { - # Some sort of audio file that doesn't have dimensions - # Don't output a no hi res message for such a file - $msgsmall = ''; - } elseif ( $this->displayImg->isVectorized() ) { - # For vectorized images, full size is just the frame size - $msgsmall = ''; - } else { - # Image is small enough to show full size on image page - $msgsmall = wfMessage( 'file-nohires' )->parse(); - } - - $params['width'] = $width; - $params['height'] = $height; - $thumbnail = $this->displayImg->transform( $params ); - Linker::processResponsiveImages( $this->displayImg, $thumbnail, $params ); - - $anchorclose = Html::rawElement( - 'div', - array( 'class' => 'mw-filepage-resolutioninfo' ), - $msgsmall - ); - - $isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1; - if ( $isMulti ) { - $out->addModules( 'mediawiki.page.image.pagination' ); - $out->addHTML( '<table class="multipageimage"><tr><td>' ); - } - - if ( $thumbnail ) { - $options = array( - 'alt' => $this->displayImg->getTitle()->getPrefixedText(), - 'file-link' => true, - ); - $out->addHTML( '<div class="fullImageLink" id="file">' . - $thumbnail->toHtml( $options ) . - $anchorclose . "</div>\n" ); - } - - if ( $isMulti ) { - $count = $this->displayImg->pageCount(); - - if ( $page > 1 ) { - $label = $out->parse( wfMessage( 'imgmultipageprev' )->text(), false ); - $link = Linker::linkKnown( - $this->getTitle(), - $label, - array(), - array( 'page' => $page - 1 ) - ); - $thumb1 = Linker::makeThumbLinkObj( - $this->getTitle(), - $this->displayImg, - $link, - $label, - 'none', - array( 'page' => $page - 1 ) - ); - } else { - $thumb1 = ''; - } - - if ( $page < $count ) { - $label = wfMessage( 'imgmultipagenext' )->text(); - $link = Linker::linkKnown( - $this->getTitle(), - $label, - array(), - array( 'page' => $page + 1 ) - ); - $thumb2 = Linker::makeThumbLinkObj( - $this->getTitle(), - $this->displayImg, - $link, - $label, - 'none', - array( 'page' => $page + 1 ) - ); - } else { - $thumb2 = ''; - } - - global $wgScript; - - $formParams = array( - 'name' => 'pageselector', - 'action' => $wgScript, - ); - $options = array(); - for ( $i = 1; $i <= $count; $i++ ) { - $options[] = Xml::option( $lang->formatNum( $i ), $i, $i == $page ); - } - $select = Xml::tags( 'select', - array( 'id' => 'pageselector', 'name' => 'page' ), - implode( "\n", $options ) ); - - $out->addHTML( - '</td><td><div class="multipageimagenavbox">' . - Xml::openElement( 'form', $formParams ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . - wfMessage( 'imgmultigoto' )->rawParams( $select )->parse() . - Xml::submitButton( wfMessage( 'imgmultigo' )->text() ) . - Xml::closeElement( 'form' ) . - "<hr />$thumb1\n$thumb2<br style=\"clear: both\" /></div></td></tr></table>" - ); - } - } elseif ( $this->displayImg->isSafeFile() ) { - # if direct link is allowed but it's not a renderable image, show an icon. - $icon = $this->displayImg->iconThumb(); - - $out->addHTML( '<div class="fullImageLink" id="file">' . - $icon->toHtml( array( 'file-link' => true ) ) . - "</div>\n" ); - } - - $longDesc = wfMessage( 'parentheses', $this->displayImg->getLongDesc() )->text(); - - $medialink = "[[Media:$filename|$linktext]]"; - - if ( !$this->displayImg->isSafeFile() ) { - $warning = wfMessage( 'mediawarning' )->plain(); - // dirmark is needed here to separate the file name, which - // most likely ends in Latin characters, from the description, - // which may begin with the file type. In RTL environment - // this will get messy. - // The dirmark, however, must not be immediately adjacent - // to the filename, because it can get copied with it. - // See bug 25277. - // @codingStandardsIgnoreStart Ignore long line - $out->addWikiText( <<<EOT -<div class="fullMedia"><span class="dangerousLink">{$medialink}</span> $dirmark<span class="fileInfo">$longDesc</span></div> -<div class="mediaWarning">$warning</div> -EOT - ); - // @codingStandardsIgnoreEnd - } else { - $out->addWikiText( <<<EOT -<div class="fullMedia">{$medialink} {$dirmark}<span class="fileInfo">$longDesc</span> -</div> -EOT - ); - } - - $renderLangOptions = $this->displayImg->getAvailableLanguages(); - if ( count( $renderLangOptions ) >= 1 ) { - $currentLanguage = $renderLang; - $defaultLang = $this->displayImg->getDefaultRenderLanguage(); - if ( is_null( $currentLanguage ) ) { - $currentLanguage = $defaultLang; - } - $out->addHtml( $this->doRenderLangOpt( $renderLangOptions, $currentLanguage, $defaultLang ) ); - } - - // Add cannot animate thumbnail warning - if ( !$this->displayImg->canAnimateThumbIfAppropriate() ) { - // Include the extension so wiki admins can - // customize it on a per file-type basis - // (aka say things like use format X instead). - // additionally have a specific message for - // file-no-thumb-animation-gif - $ext = $this->displayImg->getExtension(); - $noAnimMesg = wfMessageFallback( - 'file-no-thumb-animation-' . $ext, - 'file-no-thumb-animation' - )->plain(); - - $out->addWikiText( <<<EOT -<div class="mw-noanimatethumb">{$noAnimMesg}</div> -EOT - ); - } - - if ( !$this->displayImg->isLocal() ) { - $this->printSharedImageText(); - } - } else { - # Image does not exist - if ( !$this->getID() ) { - # No article exists either - # Show deletion log to be consistent with normal articles - LogEventsList::showLogExtract( - $out, - array( 'delete', 'move' ), - $this->getTitle()->getPrefixedText(), - '', - array( 'lim' => 10, - 'conds' => array( "log_action != 'revision'" ), - 'showIfEmpty' => false, - 'msgKey' => array( 'moveddeleted-notice' ) - ) - ); - } - - if ( $wgEnableUploads && $user->isAllowed( 'upload' ) ) { - // Only show an upload link if the user can upload - $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); - $nofile = array( - 'filepage-nofile-link', - $uploadTitle->getFullURL( array( 'wpDestFile' => $this->mPage->getFile()->getName() ) ) - ); - } else { - $nofile = 'filepage-nofile'; - } - // Note, if there is an image description page, but - // no image, then this setRobotPolicy is overridden - // by Article::View(). - $out->setRobotPolicy( 'noindex,nofollow' ); - $out->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile ); - if ( !$this->getID() && $wgSend404Code ) { - // If there is no image, no shared image, and no description page, - // output a 404, to be consistent with articles. - $request->response()->header( 'HTTP/1.1 404 Not Found' ); - } - } - $out->setFileVersion( $this->displayImg ); - } - - /** - * Creates an thumbnail of specified size and returns an HTML link to it - * @param array $params Scaler parameters - * @param int $width - * @param int $height - * @return string - */ - private function makeSizeLink( $params, $width, $height ) { - $params['width'] = $width; - $params['height'] = $height; - $thumbnail = $this->displayImg->transform( $params ); - if ( $thumbnail && !$thumbnail->isError() ) { - return Html::rawElement( 'a', array( - 'href' => $thumbnail->getUrl(), - 'class' => 'mw-thumbnail-link' - ), wfMessage( 'show-big-image-size' )->numParams( - $thumbnail->getWidth(), $thumbnail->getHeight() - )->parse() ); - } else { - return ''; - } - } - - /** - * Show a notice that the file is from a shared repository - */ - protected function printSharedImageText() { - $out = $this->getContext()->getOutput(); - $this->loadFile(); - - $descUrl = $this->mPage->getFile()->getDescriptionUrl(); - $descText = $this->mPage->getFile()->getDescriptionText( $this->getContext()->getLanguage() ); - - /* Add canonical to head if there is no local page for this shared file */ - if ( $descUrl && $this->mPage->getID() == 0 ) { - $out->setCanonicalUrl( $descUrl ); - } - - $wrap = "<div class=\"sharedUploadNotice\">\n$1\n</div>\n"; - $repo = $this->mPage->getFile()->getRepo()->getDisplayName(); - - if ( $descUrl && $descText && wfMessage( 'sharedupload-desc-here' )->plain() !== '-' ) { - $out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-here', $repo, $descUrl ) ); - } elseif ( $descUrl && wfMessage( 'sharedupload-desc-there' )->plain() !== '-' ) { - $out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-there', $repo, $descUrl ) ); - } else { - $out->wrapWikiMsg( $wrap, array( 'sharedupload', $repo ), ''/*BACKCOMPAT*/ ); - } - - if ( $descText ) { - $this->mExtraDescription = $descText; - } - } - - public function getUploadUrl() { - $this->loadFile(); - $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); - return $uploadTitle->getFullURL( array( - 'wpDestFile' => $this->mPage->getFile()->getName(), - 'wpForReUpload' => 1 - ) ); - } - - /** - * Print out the various links at the bottom of the image page, e.g. reupload, - * external editing (and instructions link) etc. - */ - protected function uploadLinksBox() { - global $wgEnableUploads; - - if ( !$wgEnableUploads ) { - return; - } - - $this->loadFile(); - if ( !$this->mPage->getFile()->isLocal() ) { - return; - } - - $out = $this->getContext()->getOutput(); - $out->addHTML( "<ul>\n" ); - - # "Upload a new version of this file" link - $canUpload = $this->getTitle()->userCan( 'upload', $this->getContext()->getUser() ); - if ( $canUpload && UploadBase::userCanReUpload( - $this->getContext()->getUser(), - $this->mPage->getFile()->name ) - ) { - $ulink = Linker::makeExternalLink( - $this->getUploadUrl(), - wfMessage( 'uploadnewversion-linktext' )->text() - ); - $out->addHTML( "<li id=\"mw-imagepage-reupload-link\">" - . "<div class=\"plainlinks\">{$ulink}</div></li>\n" ); - } else { - $out->addHTML( "<li id=\"mw-imagepage-upload-disallowed\">" - . $this->getContext()->msg( 'upload-disallowed-here' )->escaped() . "</li>\n" ); - } - - $out->addHTML( "</ul>\n" ); - } - - /** - * For overloading - */ - protected function closeShowImage() { - } - - /** - * If the page we've just displayed is in the "Image" namespace, - * we follow it with an upload history of the image and its usage. - */ - protected function imageHistory() { - $this->loadFile(); - $out = $this->getContext()->getOutput(); - $pager = new ImageHistoryPseudoPager( $this ); - $out->addHTML( $pager->getBody() ); - $out->preventClickjacking( $pager->getPreventClickjacking() ); - - $this->mPage->getFile()->resetHistory(); // free db resources - - # Exist check because we don't want to show this on pages where an image - # doesn't exist along with the noimage message, that would suck. -ævar - if ( $this->mPage->getFile()->exists() ) { - $this->uploadLinksBox(); - } - } - - /** - * @param string $target - * @param int $limit - * @return ResultWrapper - */ - protected function queryImageLinks( $target, $limit ) { - $dbr = wfGetDB( DB_SLAVE ); - - return $dbr->select( - array( 'imagelinks', 'page' ), - array( 'page_namespace', 'page_title', 'il_to' ), - array( 'il_to' => $target, 'il_from = page_id' ), - __METHOD__, - array( 'LIMIT' => $limit + 1, 'ORDER BY' => 'il_from', ) - ); - } - - protected function imageLinks() { - $limit = 100; - - $out = $this->getContext()->getOutput(); - - $rows = array(); - $redirects = array(); - foreach ( $this->getTitle()->getRedirectsHere( NS_FILE ) as $redir ) { - $redirects[$redir->getDBkey()] = array(); - $rows[] = (object)array( - 'page_namespace' => NS_FILE, - 'page_title' => $redir->getDBkey(), - ); - } - - $res = $this->queryImageLinks( $this->getTitle()->getDBkey(), $limit + 1 ); - foreach ( $res as $row ) { - $rows[] = $row; - } - $count = count( $rows ); - - $hasMore = $count > $limit; - if ( !$hasMore && count( $redirects ) ) { - $res = $this->queryImageLinks( array_keys( $redirects ), - $limit - count( $rows ) + 1 ); - foreach ( $res as $row ) { - $redirects[$row->il_to][] = $row; - $count++; - } - $hasMore = ( $res->numRows() + count( $rows ) ) > $limit; - } - - if ( $count == 0 ) { - $out->wrapWikiMsg( - Html::rawElement( 'div', - array( 'id' => 'mw-imagepage-nolinkstoimage' ), "\n$1\n" ), - 'nolinkstoimage' - ); - return; - } - - $out->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" ); - if ( !$hasMore ) { - $out->addWikiMsg( 'linkstoimage', $count ); - } else { - // More links than the limit. Add a link to [[Special:Whatlinkshere]] - $out->addWikiMsg( 'linkstoimage-more', - $this->getContext()->getLanguage()->formatNum( $limit ), - $this->getTitle()->getPrefixedDBkey() - ); - } - - $out->addHTML( - Html::openElement( 'ul', - array( 'class' => 'mw-imagepage-linkstoimage' ) ) . "\n" - ); - $count = 0; - - // Sort the list by namespace:title - usort( $rows, array( $this, 'compare' ) ); - - // Create links for every element - $currentCount = 0; - foreach ( $rows as $element ) { - $currentCount++; - if ( $currentCount > $limit ) { - break; - } - - $query = array(); - # Add a redirect=no to make redirect pages reachable - if ( isset( $redirects[$element->page_title] ) ) { - $query['redirect'] = 'no'; - } - $link = Linker::linkKnown( - Title::makeTitle( $element->page_namespace, $element->page_title ), - null, array(), $query - ); - if ( !isset( $redirects[$element->page_title] ) ) { - # No redirects - $liContents = $link; - } elseif ( count( $redirects[$element->page_title] ) === 0 ) { - # Redirect without usages - $liContents = wfMessage( 'linkstoimage-redirect' )->rawParams( $link, '' )->parse(); - } else { - # Redirect with usages - $li = ''; - foreach ( $redirects[$element->page_title] as $row ) { - $currentCount++; - if ( $currentCount > $limit ) { - break; - } - - $link2 = Linker::linkKnown( Title::makeTitle( $row->page_namespace, $row->page_title ) ); - $li .= Html::rawElement( - 'li', - array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ), - $link2 - ) . "\n"; - } - - $ul = Html::rawElement( - 'ul', - array( 'class' => 'mw-imagepage-redirectstofile' ), - $li - ) . "\n"; - $liContents = wfMessage( 'linkstoimage-redirect' )->rawParams( - $link, $ul )->parse(); - } - $out->addHTML( Html::rawElement( - 'li', - array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ), - $liContents - ) . "\n" - ); - - }; - $out->addHTML( Html::closeElement( 'ul' ) . "\n" ); - $res->free(); - - // Add a links to [[Special:Whatlinkshere]] - if ( $count > $limit ) { - $out->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() ); - } - $out->addHTML( Html::closeElement( 'div' ) . "\n" ); - } - - protected function imageDupes() { - $this->loadFile(); - $out = $this->getContext()->getOutput(); - - $dupes = $this->mPage->getDuplicates(); - if ( count( $dupes ) == 0 ) { - return; - } - - $out->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" ); - $out->addWikiMsg( 'duplicatesoffile', - $this->getContext()->getLanguage()->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey() - ); - $out->addHTML( "<ul class='mw-imagepage-duplicates'>\n" ); - - /** - * @var $file File - */ - foreach ( $dupes as $file ) { - $fromSrc = ''; - if ( $file->isLocal() ) { - $link = Linker::linkKnown( $file->getTitle() ); - } else { - $link = Linker::makeExternalLink( $file->getDescriptionUrl(), - $file->getTitle()->getPrefixedText() ); - $fromSrc = wfMessage( 'shared-repo-from', $file->getRepo()->getDisplayName() )->text(); - } - $out->addHTML( "<li>{$link} {$fromSrc}</li>\n" ); - } - $out->addHTML( "</ul></div>\n" ); - } - - /** - * Delete the file, or an earlier version of it - */ - public function delete() { - $file = $this->mPage->getFile(); - if ( !$file->exists() || !$file->isLocal() || $file->getRedirected() ) { - // Standard article deletion - parent::delete(); - return; - } - - $deleter = new FileDeleteForm( $file ); - $deleter->execute(); - } - - /** - * Display an error with a wikitext description - * - * @param string $description - */ - function showError( $description ) { - $out = $this->getContext()->getOutput(); - $out->setPageTitle( wfMessage( 'internalerror' ) ); - $out->setRobotPolicy( 'noindex,nofollow' ); - $out->setArticleRelated( false ); - $out->enableClientCache( false ); - $out->addWikiText( $description ); - } - - /** - * Callback for usort() to do link sorts by (namespace, title) - * Function copied from Title::compare() - * - * @param object $a Object page to compare with - * @param object $b Object page to compare with - * @return int Result of string comparison, or namespace comparison - */ - protected function compare( $a, $b ) { - if ( $a->page_namespace == $b->page_namespace ) { - return strcmp( $a->page_title, $b->page_title ); - } else { - return $a->page_namespace - $b->page_namespace; - } - } - - /** - * Returns the corresponding $wgImageLimits entry for the selected user option - * - * @param User $user - * @param string $optionName Name of a option to check, typically imagesize or thumbsize - * @return array - * @since 1.21 - */ - public function getImageLimitsFromOption( $user, $optionName ) { - global $wgImageLimits; - - $option = $user->getIntOption( $optionName ); - if ( !isset( $wgImageLimits[$option] ) ) { - $option = User::getDefaultOption( $optionName ); - } - - // The user offset might still be incorrect, specially if - // $wgImageLimits got changed (see bug #8858). - if ( !isset( $wgImageLimits[$option] ) ) { - // Default to the first offset in $wgImageLimits - $option = 0; - } - - return isset( $wgImageLimits[$option] ) - ? $wgImageLimits[$option] - : array( 800, 600 ); // if nothing is set, fallback to a hardcoded default - } - - /** - * Output a drop-down box for language options for the file - * - * @param array $langChoices Array of string language codes - * @param string $curLang Language code file is being viewed in. - * @param string $defaultLang Language code that image is rendered in by default - * @return string HTML to insert underneath image. - */ - protected function doRenderLangOpt( array $langChoices, $curLang, $defaultLang ) { - global $wgScript; - sort( $langChoices ); - $curLang = wfBCP47( $curLang ); - $defaultLang = wfBCP47( $defaultLang ); - $opts = ''; - $haveCurrentLang = false; - $haveDefaultLang = false; - - // We make a list of all the language choices in the file. - // Additionally if the default language to render this file - // is not included as being in this file (for example, in svgs - // usually the fallback content is the english content) also - // include a choice for that. Last of all, if we're viewing - // the file in a language not on the list, add it as a choice. - foreach ( $langChoices as $lang ) { - $code = wfBCP47( $lang ); - $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() ); - if ( $name !== '' ) { - $display = wfMessage( 'img-lang-opt', $code, $name )->text(); - } else { - $display = $code; - } - $opts .= "\n" . Xml::option( $display, $code, $curLang === $code ); - if ( $curLang === $code ) { - $haveCurrentLang = true; - } - if ( $defaultLang === $code ) { - $haveDefaultLang = true; - } - } - if ( !$haveDefaultLang ) { - // Its hard to know if the content is really in the default language, or - // if its just unmarked content that could be in any language. - $opts = Xml::option( - wfMessage( 'img-lang-default' )->text(), - $defaultLang, - $defaultLang === $curLang - ) . $opts; - } - if ( !$haveCurrentLang && $defaultLang !== $curLang ) { - $name = Language::fetchLanguageName( $curLang, $this->getContext()->getLanguage()->getCode() ); - if ( $name !== '' ) { - $display = wfMessage( 'img-lang-opt', $curLang, $name )->text(); - } else { - $display = $curLang; - } - $opts = Xml::option( $display, $curLang, true ) . $opts; - } - - $select = Html::rawElement( - 'select', - array( 'id' => 'mw-imglangselector', 'name' => 'lang' ), - $opts - ); - $submit = Xml::submitButton( wfMessage( 'img-lang-go' )->text() ); - - $formContents = wfMessage( 'img-lang-info' )->rawParams( $select, $submit )->parse() - . Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ); - - $langSelectLine = Html::rawElement( 'div', array( 'id' => 'mw-imglangselector-line' ), - Html::rawElement( 'form', array( 'action' => $wgScript ), $formContents ) - ); - return $langSelectLine; - } -} - -/** - * Builds the image revision log shown on image pages - * - * @ingroup Media - */ -class ImageHistoryList extends ContextSource { - - /** - * @var Title - */ - protected $title; - - /** - * @var File - */ - protected $img; - - /** - * @var ImagePage - */ - protected $imagePage; - - /** - * @var File - */ - protected $current; - - protected $repo, $showThumb; - protected $preventClickjacking = false; - - /** - * @param ImagePage $imagePage - */ - public function __construct( $imagePage ) { - global $wgShowArchiveThumbnails; - $this->current = $imagePage->getFile(); - $this->img = $imagePage->getDisplayedFile(); - $this->title = $imagePage->getTitle(); - $this->imagePage = $imagePage; - $this->showThumb = $wgShowArchiveThumbnails && $this->img->canRender(); - $this->setContext( $imagePage->getContext() ); - } - - /** - * @return ImagePage - */ - public function getImagePage() { - return $this->imagePage; - } - - /** - * @return File - */ - public function getFile() { - return $this->img; - } - - /** - * @param string $navLinks - * @return string - */ - public function beginImageHistoryList( $navLinks = '' ) { - return Xml::element( 'h2', array( 'id' => 'filehistory' ), $this->msg( 'filehist' )->text() ) - . "\n" - . "<div id=\"mw-imagepage-section-filehistory\">\n" - . $this->msg( 'filehist-help' )->parseAsBlock() - . $navLinks . "\n" - . Xml::openElement( 'table', array( 'class' => 'wikitable filehistory' ) ) . "\n" - . '<tr><td></td>' - . ( $this->current->isLocal() - && ( $this->getUser()->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<td></td>' : '' ) - . '<th>' . $this->msg( 'filehist-datetime' )->escaped() . '</th>' - . ( $this->showThumb ? '<th>' . $this->msg( 'filehist-thumb' )->escaped() . '</th>' : '' ) - . '<th>' . $this->msg( 'filehist-dimensions' )->escaped() . '</th>' - . '<th>' . $this->msg( 'filehist-user' )->escaped() . '</th>' - . '<th>' . $this->msg( 'filehist-comment' )->escaped() . '</th>' - . "</tr>\n"; - } - - /** - * @param string $navLinks - * @return string - */ - public function endImageHistoryList( $navLinks = '' ) { - return "</table>\n$navLinks\n</div>\n"; - } - - /** - * @param bool $iscur - * @param File $file - * @return string - */ - public function imageHistoryLine( $iscur, $file ) { - global $wgContLang; - - $user = $this->getUser(); - $lang = $this->getLanguage(); - $timestamp = wfTimestamp( TS_MW, $file->getTimestamp() ); - $img = $iscur ? $file->getName() : $file->getArchiveName(); - $userId = $file->getUser( 'id' ); - $userText = $file->getUser( 'text' ); - $description = $file->getDescription( File::FOR_THIS_USER, $user ); - - $local = $this->current->isLocal(); - $row = $selected = ''; - - // Deletion link - if ( $local && ( $user->isAllowedAny( 'delete', 'deletedhistory' ) ) ) { - $row .= '<td>'; - # Link to remove from history - if ( $user->isAllowed( 'delete' ) ) { - $q = array( 'action' => 'delete' ); - if ( !$iscur ) { - $q['oldimage'] = $img; - } - $row .= Linker::linkKnown( - $this->title, - $this->msg( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' )->escaped(), - array(), $q - ); - } - # Link to hide content. Don't show useless link to people who cannot hide revisions. - $canHide = $user->isAllowed( 'deleterevision' ); - if ( $canHide || ( $user->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) { - if ( $user->isAllowed( 'delete' ) ) { - $row .= '<br />'; - } - // If file is top revision or locked from this user, don't link - if ( $iscur || !$file->userCan( File::DELETED_RESTRICTED, $user ) ) { - $del = Linker::revDeleteLinkDisabled( $canHide ); - } else { - list( $ts, ) = explode( '!', $img, 2 ); - $query = array( - 'type' => 'oldimage', - 'target' => $this->title->getPrefixedText(), - 'ids' => $ts, - ); - $del = Linker::revDeleteLink( $query, - $file->isDeleted( File::DELETED_RESTRICTED ), $canHide ); - } - $row .= $del; - } - $row .= '</td>'; - } - - // Reversion link/current indicator - $row .= '<td>'; - if ( $iscur ) { - $row .= $this->msg( 'filehist-current' )->escaped(); - } elseif ( $local && $this->title->quickUserCan( 'edit', $user ) - && $this->title->quickUserCan( 'upload', $user ) - ) { - if ( $file->isDeleted( File::DELETED_FILE ) ) { - $row .= $this->msg( 'filehist-revert' )->escaped(); - } else { - $row .= Linker::linkKnown( - $this->title, - $this->msg( 'filehist-revert' )->escaped(), - array(), - array( - 'action' => 'revert', - 'oldimage' => $img, - 'wpEditToken' => $user->getEditToken( $img ) - ) - ); - } - } - $row .= '</td>'; - - // Date/time and image link - if ( $file->getTimestamp() === $this->img->getTimestamp() ) { - $selected = "class='filehistory-selected'"; - } - $row .= "<td $selected style='white-space: nowrap;'>"; - if ( !$file->userCan( File::DELETED_FILE, $user ) ) { - # Don't link to unviewable files - $row .= '<span class="history-deleted">' - . $lang->userTimeAndDate( $timestamp, $user ) . '</span>'; - } elseif ( $file->isDeleted( File::DELETED_FILE ) ) { - if ( $local ) { - $this->preventClickjacking(); - $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); - # Make a link to review the image - $url = Linker::linkKnown( - $revdel, - $lang->userTimeAndDate( $timestamp, $user ), - array(), - array( - 'target' => $this->title->getPrefixedText(), - 'file' => $img, - 'token' => $user->getEditToken( $img ) - ) - ); - } else { - $url = $lang->userTimeAndDate( $timestamp, $user ); - } - $row .= '<span class="history-deleted">' . $url . '</span>'; - } else { - $url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img ); - $row .= Xml::element( - 'a', - array( 'href' => $url ), - $lang->userTimeAndDate( $timestamp, $user ) - ); - } - $row .= "</td>"; - - // Thumbnail - if ( $this->showThumb ) { - $row .= '<td>' . $this->getThumbForLine( $file ) . '</td>'; - } - - // Image dimensions + size - $row .= '<td>'; - $row .= htmlspecialchars( $file->getDimensionsString() ); - $row .= $this->msg( 'word-separator' )->escaped(); - $row .= '<span style="white-space: nowrap;">'; - $row .= $this->msg( 'parentheses' )->sizeParams( $file->getSize() )->escaped(); - $row .= '</span>'; - $row .= '</td>'; - - // Uploading user - $row .= '<td>'; - // Hide deleted usernames - if ( $file->isDeleted( File::DELETED_USER ) ) { - $row .= '<span class="history-deleted">' - . $this->msg( 'rev-deleted-user' )->escaped() . '</span>'; - } else { - if ( $local ) { - $row .= Linker::userLink( $userId, $userText ); - $row .= $this->msg( 'word-separator' )->escaped(); - $row .= '<span style="white-space: nowrap;">'; - $row .= Linker::userToolLinks( $userId, $userText ); - $row .= '</span>'; - } else { - $row .= htmlspecialchars( $userText ); - } - } - $row .= '</td>'; - - // Don't show deleted descriptions - if ( $file->isDeleted( File::DELETED_COMMENT ) ) { - $row .= '<td><span class="history-deleted">' . - $this->msg( 'rev-deleted-comment' )->escaped() . '</span></td>'; - } else { - $row .= '<td dir="' . $wgContLang->getDir() . '">' . - Linker::formatComment( $description, $this->title ) . '</td>'; - } - - $rowClass = null; - wfRunHooks( 'ImagePageFileHistoryLine', array( $this, $file, &$row, &$rowClass ) ); - $classAttr = $rowClass ? " class='$rowClass'" : ''; - - return "<tr{$classAttr}>{$row}</tr>\n"; - } - - /** - * @param File $file - * @return string - */ - protected function getThumbForLine( $file ) { - $lang = $this->getLanguage(); - $user = $this->getUser(); - if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE, $user ) - && !$file->isDeleted( File::DELETED_FILE ) - ) { - $params = array( - 'width' => '120', - 'height' => '120', - ); - $timestamp = wfTimestamp( TS_MW, $file->getTimestamp() ); - - $thumbnail = $file->transform( $params ); - $options = array( - 'alt' => $this->msg( 'filehist-thumbtext', - $lang->userTimeAndDate( $timestamp, $user ), - $lang->userDate( $timestamp, $user ), - $lang->userTime( $timestamp, $user ) )->text(), - 'file-link' => true, - ); - - if ( !$thumbnail ) { - return $this->msg( 'filehist-nothumb' )->escaped(); - } - - return $thumbnail->toHtml( $options ); - } else { - return $this->msg( 'filehist-nothumb' )->escaped(); - } - } - - /** - * @param bool $enable - */ - protected function preventClickjacking( $enable = true ) { - $this->preventClickjacking = $enable; - } - - /** - * @return bool - */ - public function getPreventClickjacking() { - return $this->preventClickjacking; - } -} - -class ImageHistoryPseudoPager extends ReverseChronologicalPager { - protected $preventClickjacking = false; - - /** - * @var File - */ - protected $mImg; - - /** - * @var Title - */ - protected $mTitle; - - /** - * @param ImagePage $imagePage - */ - function __construct( $imagePage ) { - parent::__construct( $imagePage->getContext() ); - $this->mImagePage = $imagePage; - $this->mTitle = clone ( $imagePage->getTitle() ); - $this->mTitle->setFragment( '#filehistory' ); - $this->mImg = null; - $this->mHist = array(); - $this->mRange = array( 0, 0 ); // display range - } - - /** - * @return Title - */ - function getTitle() { - return $this->mTitle; - } - - function getQueryInfo() { - return false; - } - - /** - * @return string - */ - function getIndexField() { - return ''; - } - - /** - * @param object $row - * @return string - */ - function formatRow( $row ) { - return ''; - } - - /** - * @return string - */ - function getBody() { - $s = ''; - $this->doQuery(); - if ( count( $this->mHist ) ) { - $list = new ImageHistoryList( $this->mImagePage ); - # Generate prev/next links - $navLink = $this->getNavigationBar(); - $s = $list->beginImageHistoryList( $navLink ); - // Skip rows there just for paging links - for ( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) { - $file = $this->mHist[$i]; - $s .= $list->imageHistoryLine( !$file->isOld(), $file ); - } - $s .= $list->endImageHistoryList( $navLink ); - - if ( $list->getPreventClickjacking() ) { - $this->preventClickjacking(); - } - } - return $s; - } - - function doQuery() { - if ( $this->mQueryDone ) { - return; - } - $this->mImg = $this->mImagePage->getFile(); // ensure loading - if ( !$this->mImg->exists() ) { - return; - } - $queryLimit = $this->mLimit + 1; // limit plus extra row - if ( $this->mIsBackwards ) { - // Fetch the file history - $this->mHist = $this->mImg->getHistory( $queryLimit, null, $this->mOffset, false ); - // The current rev may not meet the offset/limit - $numRows = count( $this->mHist ); - if ( $numRows <= $this->mLimit && $this->mImg->getTimestamp() > $this->mOffset ) { - $this->mHist = array_merge( array( $this->mImg ), $this->mHist ); - } - } else { - // The current rev may not meet the offset - if ( !$this->mOffset || $this->mImg->getTimestamp() < $this->mOffset ) { - $this->mHist[] = $this->mImg; - } - // Old image versions (fetch extra row for nav links) - $oiLimit = count( $this->mHist ) ? $this->mLimit : $this->mLimit + 1; - // Fetch the file history - $this->mHist = array_merge( $this->mHist, - $this->mImg->getHistory( $oiLimit, $this->mOffset, null, false ) ); - } - $numRows = count( $this->mHist ); // Total number of query results - if ( $numRows ) { - # Index value of top item in the list - $firstIndex = $this->mIsBackwards ? - $this->mHist[$numRows - 1]->getTimestamp() : $this->mHist[0]->getTimestamp(); - # Discard the extra result row if there is one - if ( $numRows > $this->mLimit && $numRows > 1 ) { - if ( $this->mIsBackwards ) { - # Index value of item past the index - $this->mPastTheEndIndex = $this->mHist[0]->getTimestamp(); - # Index value of bottom item in the list - $lastIndex = $this->mHist[1]->getTimestamp(); - # Display range - $this->mRange = array( 1, $numRows - 1 ); - } else { - # Index value of item past the index - $this->mPastTheEndIndex = $this->mHist[$numRows - 1]->getTimestamp(); - # Index value of bottom item in the list - $lastIndex = $this->mHist[$numRows - 2]->getTimestamp(); - # Display range - $this->mRange = array( 0, $numRows - 2 ); - } - } else { - # Setting indexes to an empty string means that they will be - # omitted if they would otherwise appear in URLs. It just so - # happens that this is the right thing to do in the standard - # UI, in all the relevant cases. - $this->mPastTheEndIndex = ''; - # Index value of bottom item in the list - $lastIndex = $this->mIsBackwards ? - $this->mHist[0]->getTimestamp() : $this->mHist[$numRows - 1]->getTimestamp(); - # Display range - $this->mRange = array( 0, $numRows - 1 ); - } - } else { - $firstIndex = ''; - $lastIndex = ''; - $this->mPastTheEndIndex = ''; - } - if ( $this->mIsBackwards ) { - $this->mIsFirst = ( $numRows < $queryLimit ); - $this->mIsLast = ( $this->mOffset == '' ); - $this->mLastShown = $firstIndex; - $this->mFirstShown = $lastIndex; - } else { - $this->mIsFirst = ( $this->mOffset == '' ); - $this->mIsLast = ( $numRows < $queryLimit ); - $this->mLastShown = $lastIndex; - $this->mFirstShown = $firstIndex; - } - $this->mQueryDone = true; - } - - /** - * @param bool $enable - */ - protected function preventClickjacking( $enable = true ) { - $this->preventClickjacking = $enable; - } - - /** - * @return bool - */ - public function getPreventClickjacking() { - return $this->preventClickjacking; - } - -} diff --git a/includes/WikiCategoryPage.php b/includes/WikiCategoryPage.php deleted file mode 100644 index d38200169b..0000000000 --- a/includes/WikiCategoryPage.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php -/** - * Special handling for category pages. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Special handling for category pages - */ -class WikiCategoryPage extends WikiPage { - - /** - * Don't return a 404 for categories in use. - * In use defined as: either the actual page exists - * or the category currently has members. - * - * @return bool - */ - public function hasViewableContent() { - if ( parent::hasViewableContent() ) { - return true; - } else { - $cat = Category::newFromTitle( $this->mTitle ); - // If any of these are not 0, then has members - if ( $cat->getPageCount() - || $cat->getSubcatCount() - || $cat->getFileCount() - ) { - return true; - } - } - return false; - } -} diff --git a/includes/WikiFilePage.php b/includes/WikiFilePage.php deleted file mode 100644 index 34f15c3aa6..0000000000 --- a/includes/WikiFilePage.php +++ /dev/null @@ -1,236 +0,0 @@ -<?php -/** - * Special handling for file pages. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Special handling for file pages - * - * @ingroup Media - */ -class WikiFilePage extends WikiPage { - /** - * @var File - */ - protected $mFile = false; // !< File object - protected $mRepo = null; // !< - protected $mFileLoaded = false; // !< - protected $mDupes = null; // !< - - public function __construct( $title ) { - parent::__construct( $title ); - $this->mDupes = null; - $this->mRepo = null; - } - - public function getActionOverrides() { - $overrides = parent::getActionOverrides(); - $overrides['revert'] = 'RevertFileAction'; - return $overrides; - } - - /** - * @param File $file - */ - public function setFile( $file ) { - $this->mFile = $file; - $this->mFileLoaded = true; - } - - /** - * @return bool - */ - protected function loadFile() { - if ( $this->mFileLoaded ) { - return true; - } - $this->mFileLoaded = true; - - $this->mFile = wfFindFile( $this->mTitle ); - if ( !$this->mFile ) { - $this->mFile = wfLocalFile( $this->mTitle ); // always a File - } - $this->mRepo = $this->mFile->getRepo(); - return true; - } - - /** - * @return mixed|null|Title - */ - public function getRedirectTarget() { - $this->loadFile(); - if ( $this->mFile->isLocal() ) { - return parent::getRedirectTarget(); - } - // Foreign image page - $from = $this->mFile->getRedirected(); - $to = $this->mFile->getName(); - if ( $from == $to ) { - return null; - } - $this->mRedirectTarget = Title::makeTitle( NS_FILE, $to ); - return $this->mRedirectTarget; - } - - /** - * @return bool|mixed|Title - */ - public function followRedirect() { - $this->loadFile(); - if ( $this->mFile->isLocal() ) { - return parent::followRedirect(); - } - $from = $this->mFile->getRedirected(); - $to = $this->mFile->getName(); - if ( $from == $to ) { - return false; - } - return Title::makeTitle( NS_FILE, $to ); - } - - /** - * @return bool - */ - public function isRedirect() { - $this->loadFile(); - if ( $this->mFile->isLocal() ) { - return parent::isRedirect(); - } - - return (bool)$this->mFile->getRedirected(); - } - - /** - * @return bool - */ - public function isLocal() { - $this->loadFile(); - return $this->mFile->isLocal(); - } - - /** - * @return bool|File - */ - public function getFile() { - $this->loadFile(); - return $this->mFile; - } - - /** - * @return array|null - */ - public function getDuplicates() { - $this->loadFile(); - if ( !is_null( $this->mDupes ) ) { - return $this->mDupes; - } - $hash = $this->mFile->getSha1(); - if ( !( $hash ) ) { - $this->mDupes = array(); - return $this->mDupes; - } - $dupes = RepoGroup::singleton()->findBySha1( $hash ); - // Remove duplicates with self and non matching file sizes - $self = $this->mFile->getRepoName() . ':' . $this->mFile->getName(); - $size = $this->mFile->getSize(); - - /** - * @var $file File - */ - foreach ( $dupes as $index => $file ) { - $key = $file->getRepoName() . ':' . $file->getName(); - if ( $key == $self ) { - unset( $dupes[$index] ); - } - if ( $file->getSize() != $size ) { - unset( $dupes[$index] ); - } - } - $this->mDupes = $dupes; - return $this->mDupes; - } - - /** - * Override handling of action=purge - * @return bool - */ - public function doPurge() { - $this->loadFile(); - if ( $this->mFile->exists() ) { - wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" ); - $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ); - $update->doUpdate(); - $this->mFile->upgradeRow(); - $this->mFile->purgeCache( array( 'forThumbRefresh' => true ) ); - } else { - wfDebug( 'ImagePage::doPurge no image for ' - . $this->mFile->getName() . "; limiting purge to cache only\n" ); - // even if the file supposedly doesn't exist, force any cached information - // to be updated (in case the cached information is wrong) - $this->mFile->purgeCache( array( 'forThumbRefresh' => true ) ); - } - if ( $this->mRepo ) { - // Purge redirect cache - $this->mRepo->invalidateImageRedirect( $this->mTitle ); - } - return parent::doPurge(); - } - - /** - * Get the categories this file is a member of on the wiki where it was uploaded. - * For local files, this is the same as getCategories(). - * For foreign API files (InstantCommons), this is not supported currently. - * Results will include hidden categories. - * - * @return TitleArray|Title[] - * @since 1.23 - */ - public function getForeignCategories() { - $this->loadFile(); - $title = $this->mTitle; - $file = $this->mFile; - - if ( ! $file instanceof LocalFile ) { - wfDebug( __CLASS__ . '::' . __METHOD__ . " is not supported for this file\n" ); - return TitleArray::newFromResult( new FakeResultWrapper( array() ) ); - } - - /** @var LocalRepo $repo */ - $repo = $file->getRepo(); - $dbr = $repo->getSlaveDB(); - - $res = $dbr->select( - array( 'page', 'categorylinks' ), - array( - 'page_title' => 'cl_to', - 'page_namespace' => NS_CATEGORY, - ), - array( - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey(), - ), - __METHOD__, - array(), - array( 'categorylinks' => array( 'INNER JOIN', 'page_id = cl_from' ) ) - ); - - return TitleArray::newFromResult( $res ); - } -} diff --git a/includes/WikiPage.php b/includes/WikiPage.php deleted file mode 100644 index c4d1bf3b91..0000000000 --- a/includes/WikiPage.php +++ /dev/null @@ -1,3760 +0,0 @@ -<?php -/** - * Base representation for a MediaWiki page. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage) - */ -interface Page { -} - -/** - * Class representing a MediaWiki article and history. - * - * Some fields are public only for backwards-compatibility. Use accessors. - * In the past, this class was part of Article.php and everything was public. - * - * @internal documentation reviewed 15 Mar 2010 - */ -class WikiPage implements Page, IDBAccessObject { - // Constants for $mDataLoadedFrom and related - - /** - * @var Title - */ - public $mTitle = null; - - /**@{{ - * @protected - */ - public $mDataLoaded = false; // !< Boolean - public $mIsRedirect = false; // !< Boolean - public $mLatest = false; // !< Integer (false means "not loaded") - /**@}}*/ - - /** @var stdclass Map of cache fields (text, parser output, ect) for a proposed/new edit */ - public $mPreparedEdit = false; - - /** - * @var int - */ - protected $mId = null; - - /** - * @var int One of the READ_* constants - */ - protected $mDataLoadedFrom = self::READ_NONE; - - /** - * @var Title - */ - protected $mRedirectTarget = null; - - /** - * @var Revision - */ - protected $mLastRevision = null; - - /** - * @var string Timestamp of the current revision or empty string if not loaded - */ - protected $mTimestamp = ''; - - /** - * @var string - */ - protected $mTouched = '19700101000000'; - - /** - * @var string - */ - protected $mLinksUpdated = '19700101000000'; - - /** - * @var int|null - */ - protected $mCounter = null; - - /** - * Constructor and clear the article - * @param Title $title Reference to a Title object. - */ - public function __construct( Title $title ) { - $this->mTitle = $title; - } - - /** - * Create a WikiPage object of the appropriate class for the given title. - * - * @param Title $title - * - * @throws MWException - * @return WikiPage Object of the appropriate type - */ - public static function factory( Title $title ) { - $ns = $title->getNamespace(); - - if ( $ns == NS_MEDIA ) { - throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." ); - } elseif ( $ns < 0 ) { - throw new MWException( "Invalid or virtual namespace $ns given." ); - } - - switch ( $ns ) { - case NS_FILE: - $page = new WikiFilePage( $title ); - break; - case NS_CATEGORY: - $page = new WikiCategoryPage( $title ); - break; - default: - $page = new WikiPage( $title ); - } - - return $page; - } - - /** - * Constructor from a page id - * - * @param int $id Article ID to load - * @param string|int $from One of the following values: - * - "fromdb" or WikiPage::READ_NORMAL to select from a slave database - * - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database - * - * @return WikiPage|null - */ - public static function newFromID( $id, $from = 'fromdb' ) { - // page id's are never 0 or negative, see bug 61166 - if ( $id < 1 ) { - return null; - } - - $from = self::convertSelectType( $from ); - $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE ); - $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ ); - if ( !$row ) { - return null; - } - return self::newFromRow( $row, $from ); - } - - /** - * Constructor from a database row - * - * @since 1.20 - * @param object $row Database row containing at least fields returned by selectFields(). - * @param string|int $from Source of $data: - * - "fromdb" or WikiPage::READ_NORMAL: from a slave DB - * - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB - * - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE - * @return WikiPage - */ - public static function newFromRow( $row, $from = 'fromdb' ) { - $page = self::factory( Title::newFromRow( $row ) ); - $page->loadFromRow( $row, $from ); - return $page; - } - - /** - * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants. - * - * @param object|string|int $type - * @return mixed - */ - private static function convertSelectType( $type ) { - switch ( $type ) { - case 'fromdb': - return self::READ_NORMAL; - case 'fromdbmaster': - return self::READ_LATEST; - case 'forupdate': - return self::READ_LOCKING; - default: - // It may already be an integer or whatever else - return $type; - } - } - - /** - * Returns overrides for action handlers. - * Classes listed here will be used instead of the default one when - * (and only when) $wgActions[$action] === true. This allows subclasses - * to override the default behavior. - * - * @todo Move this UI stuff somewhere else - * - * @return array - */ - public function getActionOverrides() { - $content_handler = $this->getContentHandler(); - return $content_handler->getActionOverrides(); - } - - /** - * Returns the ContentHandler instance to be used to deal with the content of this WikiPage. - * - * Shorthand for ContentHandler::getForModelID( $this->getContentModel() ); - * - * @return ContentHandler - * - * @since 1.21 - */ - public function getContentHandler() { - return ContentHandler::getForModelID( $this->getContentModel() ); - } - - /** - * Get the title object of the article - * @return Title Title object of this page - */ - public function getTitle() { - return $this->mTitle; - } - - /** - * Clear the object - * @return void - */ - public function clear() { - $this->mDataLoaded = false; - $this->mDataLoadedFrom = self::READ_NONE; - - $this->clearCacheFields(); - } - - /** - * Clear the object cache fields - * @return void - */ - protected function clearCacheFields() { - $this->mId = null; - $this->mCounter = null; - $this->mRedirectTarget = null; // Title object if set - $this->mLastRevision = null; // Latest revision - $this->mTouched = '19700101000000'; - $this->mLinksUpdated = '19700101000000'; - $this->mTimestamp = ''; - $this->mIsRedirect = false; - $this->mLatest = false; - // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks - // the requested rev ID and content against the cached one for equality. For most - // content types, the output should not change during the lifetime of this cache. - // Clearing it can cause extra parses on edit for no reason. - } - - /** - * Clear the mPreparedEdit cache field, as may be needed by mutable content types - * @return void - * @since 1.23 - */ - public function clearPreparedEdit() { - $this->mPreparedEdit = false; - } - - /** - * Return the list of revision fields that should be selected to create - * a new page. - * - * @return array - */ - public static function selectFields() { - global $wgContentHandlerUseDB; - - $fields = array( - 'page_id', - 'page_namespace', - 'page_title', - 'page_restrictions', - 'page_counter', - 'page_is_redirect', - 'page_is_new', - 'page_random', - 'page_touched', - 'page_links_updated', - 'page_latest', - 'page_len', - ); - - if ( $wgContentHandlerUseDB ) { - $fields[] = 'page_content_model'; - } - - return $fields; - } - - /** - * Fetch a page record with the given conditions - * @param DatabaseBase $dbr - * @param array $conditions - * @param array $options - * @return object|bool Database result resource, or false on failure - */ - protected function pageData( $dbr, $conditions, $options = array() ) { - $fields = self::selectFields(); - - wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); - - $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options ); - - wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); - - return $row; - } - - /** - * Fetch a page record matching the Title object's namespace and title - * using a sanitized title string - * - * @param DatabaseBase $dbr - * @param Title $title - * @param array $options - * @return object|bool Database result resource, or false on failure - */ - public function pageDataFromTitle( $dbr, $title, $options = array() ) { - return $this->pageData( $dbr, array( - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() ), $options ); - } - - /** - * Fetch a page record matching the requested ID - * - * @param DatabaseBase $dbr - * @param int $id - * @param array $options - * @return object|bool Database result resource, or false on failure - */ - public function pageDataFromId( $dbr, $id, $options = array() ) { - return $this->pageData( $dbr, array( 'page_id' => $id ), $options ); - } - - /** - * Set the general counter, title etc data loaded from - * some source. - * - * @param object|string|int $from One of the following: - * - A DB query result object. - * - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB. - * - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB. - * - "forupdate" or WikiPage::READ_LOCKING to get from the master DB - * using SELECT FOR UPDATE. - * - * @return void - */ - public function loadPageData( $from = 'fromdb' ) { - $from = self::convertSelectType( $from ); - if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) { - // We already have the data from the correct location, no need to load it twice. - return; - } - - if ( $from === self::READ_LOCKING ) { - $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) ); - } elseif ( $from === self::READ_LATEST ) { - $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); - } elseif ( $from === self::READ_NORMAL ) { - $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); - // Use a "last rev inserted" timestamp key to diminish the issue of slave lag. - // Note that DB also stores the master position in the session and checks it. - $touched = $this->getCachedLastEditTime(); - if ( $touched ) { // key set - if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) { - $from = self::READ_LATEST; - $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); - } - } - } else { - // No idea from where the caller got this data, assume slave database. - $data = $from; - $from = self::READ_NORMAL; - } - - $this->loadFromRow( $data, $from ); - } - - /** - * Load the object from a database row - * - * @since 1.20 - * @param object $data Database row containing at least fields returned by selectFields() - * @param string|int $from One of the following: - * - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB - * - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB - * - "forupdate" or WikiPage::READ_LOCKING if the data comes from from - * the master DB using SELECT FOR UPDATE - */ - public function loadFromRow( $data, $from ) { - $lc = LinkCache::singleton(); - $lc->clearLink( $this->mTitle ); - - if ( $data ) { - $lc->addGoodLinkObjFromRow( $this->mTitle, $data ); - - $this->mTitle->loadFromRow( $data ); - - // Old-fashioned restrictions - $this->mTitle->loadRestrictions( $data->page_restrictions ); - - $this->mId = intval( $data->page_id ); - $this->mCounter = intval( $data->page_counter ); - $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); - $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated ); - $this->mIsRedirect = intval( $data->page_is_redirect ); - $this->mLatest = intval( $data->page_latest ); - // Bug 37225: $latest may no longer match the cached latest Revision object. - // Double-check the ID of any cached latest Revision object for consistency. - if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) { - $this->mLastRevision = null; - $this->mTimestamp = ''; - } - } else { - $lc->addBadLinkObj( $this->mTitle ); - - $this->mTitle->loadFromRow( false ); - - $this->clearCacheFields(); - - $this->mId = 0; - } - - $this->mDataLoaded = true; - $this->mDataLoadedFrom = self::convertSelectType( $from ); - } - - /** - * @return int Page ID - */ - public function getId() { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } - return $this->mId; - } - - /** - * @return bool Whether or not the page exists in the database - */ - public function exists() { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } - return $this->mId > 0; - } - - /** - * Check if this page is something we're going to be showing - * some sort of sensible content for. If we return false, page - * views (plain action=view) will return an HTTP 404 response, - * so spiders and robots can know they're following a bad link. - * - * @return bool - */ - public function hasViewableContent() { - return $this->exists() || $this->mTitle->isAlwaysKnown(); - } - - /** - * @return int The view count for the page - */ - public function getCount() { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } - - return $this->mCounter; - } - - /** - * Tests if the article content represents a redirect - * - * @return bool - */ - public function isRedirect() { - $content = $this->getContent(); - if ( !$content ) { - return false; - } - - return $content->isRedirect(); - } - - /** - * Returns the page's content model id (see the CONTENT_MODEL_XXX constants). - * - * Will use the revisions actual content model if the page exists, - * and the page's default if the page doesn't exist yet. - * - * @return string - * - * @since 1.21 - */ - public function getContentModel() { - if ( $this->exists() ) { - // look at the revision's actual content model - $rev = $this->getRevision(); - - if ( $rev !== null ) { - return $rev->getContentModel(); - } else { - $title = $this->mTitle->getPrefixedDBkey(); - wfWarn( "Page $title exists but has no (visible) revisions!" ); - } - } - - // use the default model for this page - return $this->mTitle->getContentModel(); - } - - /** - * Loads page_touched and returns a value indicating if it should be used - * @return bool true if not a redirect - */ - public function checkTouched() { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } - return !$this->mIsRedirect; - } - - /** - * Get the page_touched field - * @return string Containing GMT timestamp - */ - public function getTouched() { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } - return $this->mTouched; - } - - /** - * Get the page_links_updated field - * @return string|null Containing GMT timestamp - */ - public function getLinksTimestamp() { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } - return $this->mLinksUpdated; - } - - /** - * Get the page_latest field - * @return int rev_id of current revision - */ - public function getLatest() { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } - return (int)$this->mLatest; - } - - /** - * Get the Revision object of the oldest revision - * @return Revision|null - */ - public function getOldestRevision() { - wfProfileIn( __METHOD__ ); - - // Try using the slave database first, then try the master - $continue = 2; - $db = wfGetDB( DB_SLAVE ); - $revSelectFields = Revision::selectFields(); - - $row = null; - while ( $continue ) { - $row = $db->selectRow( - array( 'page', 'revision' ), - $revSelectFields, - array( - 'page_namespace' => $this->mTitle->getNamespace(), - 'page_title' => $this->mTitle->getDBkey(), - 'rev_page = page_id' - ), - __METHOD__, - array( - 'ORDER BY' => 'rev_timestamp ASC' - ) - ); - - if ( $row ) { - $continue = 0; - } else { - $db = wfGetDB( DB_MASTER ); - $continue--; - } - } - - wfProfileOut( __METHOD__ ); - return $row ? Revision::newFromRow( $row ) : null; - } - - /** - * Loads everything except the text - * This isn't necessary for all uses, so it's only done if needed. - */ - protected function loadLastEdit() { - if ( $this->mLastRevision !== null ) { - return; // already loaded - } - - $latest = $this->getLatest(); - if ( !$latest ) { - return; // page doesn't exist or is missing page_latest info - } - - // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the - // latest changes committed. This is true even within REPEATABLE-READ transactions, where - // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to - // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row - // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT. - // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read. - $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0; - $revision = Revision::newFromPageId( $this->getId(), $latest, $flags ); - if ( $revision ) { // sanity - $this->setLastEdit( $revision ); - } - } - - /** - * Set the latest revision - * @param Revision $revision - */ - protected function setLastEdit( Revision $revision ) { - $this->mLastRevision = $revision; - $this->mTimestamp = $revision->getTimestamp(); - } - - /** - * Get the latest revision - * @return Revision|null - */ - public function getRevision() { - $this->loadLastEdit(); - if ( $this->mLastRevision ) { - return $this->mLastRevision; - } - return null; - } - - /** - * Get the content of the current revision. No side-effects... - * - * @param int $audience int One of: - * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to $wgUser - * Revision::RAW get the text regardless of permissions - * @param User $user User object to check for, only if FOR_THIS_USER is passed - * to the $audience parameter - * @return Content|null The content of the current revision - * - * @since 1.21 - */ - public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) { - $this->loadLastEdit(); - if ( $this->mLastRevision ) { - return $this->mLastRevision->getContent( $audience, $user ); - } - return null; - } - - /** - * Get the text of the current revision. No side-effects... - * - * @param int $audience One of: - * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to the given user - * Revision::RAW get the text regardless of permissions - * @param User $user User object to check for, only if FOR_THIS_USER is passed - * to the $audience parameter - * @return string|bool The text of the current revision - * @deprecated since 1.21, getContent() should be used instead. - */ - public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { - ContentHandler::deprecated( __METHOD__, '1.21' ); - - $this->loadLastEdit(); - if ( $this->mLastRevision ) { - return $this->mLastRevision->getText( $audience, $user ); - } - return false; - } - - /** - * Get the text of the current revision. No side-effects... - * - * @return string|bool The text of the current revision. False on failure - * @deprecated since 1.21, getContent() should be used instead. - */ - public function getRawText() { - ContentHandler::deprecated( __METHOD__, '1.21' ); - - return $this->getText( Revision::RAW ); - } - - /** - * @return string MW timestamp of last article revision - */ - public function getTimestamp() { - // Check if the field has been filled by WikiPage::setTimestamp() - if ( !$this->mTimestamp ) { - $this->loadLastEdit(); - } - - return wfTimestamp( TS_MW, $this->mTimestamp ); - } - - /** - * Set the page timestamp (use only to avoid DB queries) - * @param string $ts MW timestamp of last article revision - * @return void - */ - public function setTimestamp( $ts ) { - $this->mTimestamp = wfTimestamp( TS_MW, $ts ); - } - - /** - * @param int $audience One of: - * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to the given user - * Revision::RAW get the text regardless of permissions - * @param User $user User object to check for, only if FOR_THIS_USER is passed - * to the $audience parameter - * @return int user ID for the user that made the last article revision - */ - public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) { - $this->loadLastEdit(); - if ( $this->mLastRevision ) { - return $this->mLastRevision->getUser( $audience, $user ); - } else { - return -1; - } - } - - /** - * Get the User object of the user who created the page - * @param int $audience One of: - * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to the given user - * Revision::RAW get the text regardless of permissions - * @param User $user User object to check for, only if FOR_THIS_USER is passed - * to the $audience parameter - * @return User|null - */ - public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) { - $revision = $this->getOldestRevision(); - if ( $revision ) { - $userName = $revision->getUserText( $audience, $user ); - return User::newFromName( $userName, false ); - } else { - return null; - } - } - - /** - * @param int $audience One of: - * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to the given user - * Revision::RAW get the text regardless of permissions - * @param User $user User object to check for, only if FOR_THIS_USER is passed - * to the $audience parameter - * @return string username of the user that made the last article revision - */ - public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) { - $this->loadLastEdit(); - if ( $this->mLastRevision ) { - return $this->mLastRevision->getUserText( $audience, $user ); - } else { - return ''; - } - } - - /** - * @param int $audience One of: - * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to the given user - * Revision::RAW get the text regardless of permissions - * @param User $user User object to check for, only if FOR_THIS_USER is passed - * to the $audience parameter - * @return string Comment stored for the last article revision - */ - public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) { - $this->loadLastEdit(); - if ( $this->mLastRevision ) { - return $this->mLastRevision->getComment( $audience, $user ); - } else { - return ''; - } - } - - /** - * Returns true if last revision was marked as "minor edit" - * - * @return bool Minor edit indicator for the last article revision. - */ - public function getMinorEdit() { - $this->loadLastEdit(); - if ( $this->mLastRevision ) { - return $this->mLastRevision->isMinor(); - } else { - return false; - } - } - - /** - * Get the cached timestamp for the last time the page changed. - * This is only used to help handle slave lag by comparing to page_touched. - * @return string MW timestamp - */ - protected function getCachedLastEditTime() { - global $wgMemc; - $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); - return $wgMemc->get( $key ); - } - - /** - * Set the cached timestamp for the last time the page changed. - * This is only used to help handle slave lag by comparing to page_touched. - * @param string $timestamp - * @return void - */ - public function setCachedLastEditTime( $timestamp ) { - global $wgMemc; - $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); - $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60 * 15 ); - } - - /** - * Determine whether a page would be suitable for being counted as an - * article in the site_stats table based on the title & its content - * - * @param object|bool $editInfo (false): object returned by prepareTextForEdit(), - * if false, the current database state will be used - * @return bool - */ - public function isCountable( $editInfo = false ) { - global $wgArticleCountMethod; - - if ( !$this->mTitle->isContentPage() ) { - return false; - } - - if ( $editInfo ) { - $content = $editInfo->pstContent; - } else { - $content = $this->getContent(); - } - - if ( !$content || $content->isRedirect() ) { - return false; - } - - $hasLinks = null; - - if ( $wgArticleCountMethod === 'link' ) { - // nasty special case to avoid re-parsing to detect links - - if ( $editInfo ) { - // ParserOutput::getLinks() is a 2D array of page links, so - // to be really correct we would need to recurse in the array - // but the main array should only have items in it if there are - // links. - $hasLinks = (bool)count( $editInfo->output->getLinks() ); - } else { - $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, - array( 'pl_from' => $this->getId() ), __METHOD__ ); - } - } - - return $content->isCountable( $hasLinks ); - } - - /** - * If this page is a redirect, get its target - * - * The target will be fetched from the redirect table if possible. - * If this page doesn't have an entry there, call insertRedirect() - * @return Title|null Title object, or null if this page is not a redirect - */ - public function getRedirectTarget() { - if ( !$this->mTitle->isRedirect() ) { - return null; - } - - if ( $this->mRedirectTarget !== null ) { - return $this->mRedirectTarget; - } - - // Query the redirect table - $dbr = wfGetDB( DB_SLAVE ); - $row = $dbr->selectRow( 'redirect', - array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ), - array( 'rd_from' => $this->getId() ), - __METHOD__ - ); - - // rd_fragment and rd_interwiki were added later, populate them if empty - if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) { - $this->mRedirectTarget = Title::makeTitle( - $row->rd_namespace, $row->rd_title, - $row->rd_fragment, $row->rd_interwiki ); - return $this->mRedirectTarget; - } - - // This page doesn't have an entry in the redirect table - $this->mRedirectTarget = $this->insertRedirect(); - return $this->mRedirectTarget; - } - - /** - * Insert an entry for this page into the redirect table. - * - * Don't call this function directly unless you know what you're doing. - * @return Title|null Title object or null if not a redirect - */ - public function insertRedirect() { - // recurse through to only get the final target - $content = $this->getContent(); - $retval = $content ? $content->getUltimateRedirectTarget() : null; - if ( !$retval ) { - return null; - } - $this->insertRedirectEntry( $retval ); - return $retval; - } - - /** - * Insert or update the redirect table entry for this page to indicate - * it redirects to $rt . - * @param Title $rt Redirect target - */ - public function insertRedirectEntry( $rt ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'redirect', array( 'rd_from' ), - array( - 'rd_from' => $this->getId(), - 'rd_namespace' => $rt->getNamespace(), - 'rd_title' => $rt->getDBkey(), - 'rd_fragment' => $rt->getFragment(), - 'rd_interwiki' => $rt->getInterwiki(), - ), - __METHOD__ - ); - } - - /** - * Get the Title object or URL this page redirects to - * - * @return bool|Title|string false, Title of in-wiki target, or string with URL - */ - public function followRedirect() { - return $this->getRedirectURL( $this->getRedirectTarget() ); - } - - /** - * Get the Title object or URL to use for a redirect. We use Title - * objects for same-wiki, non-special redirects and URLs for everything - * else. - * @param Title $rt Redirect target - * @return bool|Title|string false, Title object of local target, or string with URL - */ - public function getRedirectURL( $rt ) { - if ( !$rt ) { - return false; - } - - if ( $rt->isExternal() ) { - if ( $rt->isLocal() ) { - // Offsite wikis need an HTTP redirect. - // - // This can be hard to reverse and may produce loops, - // so they may be disabled in the site configuration. - $source = $this->mTitle->getFullURL( 'redirect=no' ); - return $rt->getFullURL( array( 'rdfrom' => $source ) ); - } else { - // External pages pages without "local" bit set are not valid - // redirect targets - return false; - } - } - - if ( $rt->isSpecialPage() ) { - // Gotta handle redirects to special pages differently: - // Fill the HTTP response "Location" header and ignore - // the rest of the page we're on. - // - // Some pages are not valid targets - if ( $rt->isValidRedirectTarget() ) { - return $rt->getFullURL(); - } else { - return false; - } - } - - return $rt; - } - - /** - * Get a list of users who have edited this article, not including the user who made - * the most recent revision, which you can get from $article->getUser() if you want it - * @return UserArrayFromResult - */ - public function getContributors() { - // @todo FIXME: This is expensive; cache this info somewhere. - - $dbr = wfGetDB( DB_SLAVE ); - - if ( $dbr->implicitGroupby() ) { - $realNameField = 'user_real_name'; - } else { - $realNameField = 'MIN(user_real_name) AS user_real_name'; - } - - $tables = array( 'revision', 'user' ); - - $fields = array( - 'user_id' => 'rev_user', - 'user_name' => 'rev_user_text', - $realNameField, - 'timestamp' => 'MAX(rev_timestamp)', - ); - - $conds = array( 'rev_page' => $this->getId() ); - - // The user who made the top revision gets credited as "this page was last edited by - // John, based on contributions by Tom, Dick and Harry", so don't include them twice. - $user = $this->getUser(); - if ( $user ) { - $conds[] = "rev_user != $user"; - } else { - $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}"; - } - - $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden? - - $jconds = array( - 'user' => array( 'LEFT JOIN', 'rev_user = user_id' ), - ); - - $options = array( - 'GROUP BY' => array( 'rev_user', 'rev_user_text' ), - 'ORDER BY' => 'timestamp DESC', - ); - - $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds ); - return new UserArrayFromResult( $res ); - } - - /** - * Get the last N authors - * @param int $num Number of revisions to get - * @param int|string $revLatest The latest rev_id, selected from the master (optional) - * @return array Array of authors, duplicates not removed - */ - public function getLastNAuthors( $num, $revLatest = 0 ) { - wfProfileIn( __METHOD__ ); - // First try the slave - // If that doesn't have the latest revision, try the master - $continue = 2; - $db = wfGetDB( DB_SLAVE ); - - do { - $res = $db->select( array( 'page', 'revision' ), - array( 'rev_id', 'rev_user_text' ), - array( - 'page_namespace' => $this->mTitle->getNamespace(), - 'page_title' => $this->mTitle->getDBkey(), - 'rev_page = page_id' - ), __METHOD__, - array( - 'ORDER BY' => 'rev_timestamp DESC', - 'LIMIT' => $num - ) - ); - - if ( !$res ) { - wfProfileOut( __METHOD__ ); - return array(); - } - - $row = $db->fetchObject( $res ); - - if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { - $db = wfGetDB( DB_MASTER ); - $continue--; - } else { - $continue = 0; - } - } while ( $continue ); - - $authors = array( $row->rev_user_text ); - - foreach ( $res as $row ) { - $authors[] = $row->rev_user_text; - } - - wfProfileOut( __METHOD__ ); - return $authors; - } - - /** - * Should the parser cache be used? - * - * @param ParserOptions $parserOptions ParserOptions to check - * @param int $oldid - * @return bool - */ - public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) { - global $wgEnableParserCache; - - return $wgEnableParserCache - && $parserOptions->getStubThreshold() == 0 - && $this->exists() - && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) - && $this->getContentHandler()->isParserCacheSupported(); - } - - /** - * Get a ParserOutput for the given ParserOptions and revision ID. - * The parser cache will be used if possible. - * - * @since 1.19 - * @param ParserOptions $parserOptions ParserOptions to use for the parse operation - * @param null|int $oldid Revision ID to get the text from, passing null or 0 will - * get the current revision (default value) - * - * @return ParserOutput|bool ParserOutput or false if the revision was not found - */ - public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { - wfProfileIn( __METHOD__ ); - - $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid ); - wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); - if ( $parserOptions->getStubThreshold() ) { - wfIncrStats( 'pcache_miss_stub' ); - } - - if ( $useParserCache ) { - $parserOutput = ParserCache::singleton()->get( $this, $parserOptions ); - if ( $parserOutput !== false ) { - wfProfileOut( __METHOD__ ); - return $parserOutput; - } - } - - if ( $oldid === null || $oldid === 0 ) { - $oldid = $this->getLatest(); - } - - $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache ); - $pool->execute(); - - wfProfileOut( __METHOD__ ); - - return $pool->getParserOutput(); - } - - /** - * Do standard deferred updates after page view (existing or missing page) - * @param User $user The relevant user - * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed. - */ - public function doViewUpdates( User $user, $oldid = 0 ) { - global $wgDisableCounters; - if ( wfReadOnly() ) { - return; - } - - // Don't update page view counters on views from bot users (bug 14044) - if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->exists() ) { - DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) ); - DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) ); - } - - // Update newtalk / watchlist notification status - $user->clearNotification( $this->mTitle, $oldid ); - } - - /** - * Perform the actions of a page purging - * @return bool - */ - public function doPurge() { - global $wgUseSquid; - - if ( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { - return false; - } - - // Invalidate the cache - $this->mTitle->invalidateCache(); - - if ( $wgUseSquid ) { - // Commit the transaction before the purge is sent - $dbw = wfGetDB( DB_MASTER ); - $dbw->commit( __METHOD__ ); - - // Send purge - $update = SquidUpdate::newSimplePurge( $this->mTitle ); - $update->doUpdate(); - } - - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - // @todo move this logic to MessageCache - - if ( $this->exists() ) { - // NOTE: use transclusion text for messages. - // This is consistent with MessageCache::getMsgFromNamespace() - - $content = $this->getContent(); - $text = $content === null ? null : $content->getWikitextForTransclusion(); - - if ( $text === null ) { - $text = false; - } - } else { - $text = false; - } - - MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text ); - } - return true; - } - - /** - * Insert a new empty page record for this article. - * This *must* be followed up by creating a revision - * and running $this->updateRevisionOn( ... ); - * or else the record will be left in a funky state. - * Best if all done inside a transaction. - * - * @param DatabaseBase $dbw - * @return int The newly created page_id key, or false if the title already existed - */ - public function insertOn( $dbw ) { - wfProfileIn( __METHOD__ ); - - $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); - $dbw->insert( 'page', array( - 'page_id' => $page_id, - 'page_namespace' => $this->mTitle->getNamespace(), - 'page_title' => $this->mTitle->getDBkey(), - 'page_counter' => 0, - 'page_restrictions' => '', - 'page_is_redirect' => 0, // Will set this shortly... - 'page_is_new' => 1, - 'page_random' => wfRandom(), - 'page_touched' => $dbw->timestamp(), - 'page_latest' => 0, // Fill this in shortly... - 'page_len' => 0, // Fill this in shortly... - ), __METHOD__, 'IGNORE' ); - - $affected = $dbw->affectedRows(); - - if ( $affected ) { - $newid = $dbw->insertId(); - $this->mId = $newid; - $this->mTitle->resetArticleID( $newid ); - } - wfProfileOut( __METHOD__ ); - - return $affected ? $newid : false; - } - - /** - * Update the page record to point to a newly saved revision. - * - * @param DatabaseBase $dbw - * @param Revision $revision For ID number, and text used to set - * length and redirect status fields - * @param int $lastRevision If given, will not overwrite the page field - * when different from the currently set value. - * Giving 0 indicates the new page flag should be set on. - * @param bool $lastRevIsRedirect If given, will optimize adding and - * removing rows in redirect table. - * @return bool true on success, false on failure - */ - public function updateRevisionOn( $dbw, $revision, $lastRevision = null, - $lastRevIsRedirect = null - ) { - global $wgContentHandlerUseDB; - - wfProfileIn( __METHOD__ ); - - $content = $revision->getContent(); - $len = $content ? $content->getSize() : 0; - $rt = $content ? $content->getUltimateRedirectTarget() : null; - - $conditions = array( 'page_id' => $this->getId() ); - - if ( !is_null( $lastRevision ) ) { - // An extra check against threads stepping on each other - $conditions['page_latest'] = $lastRevision; - } - - $now = wfTimestampNow(); - $row = array( /* SET */ - 'page_latest' => $revision->getId(), - 'page_touched' => $dbw->timestamp( $now ), - 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, - 'page_is_redirect' => $rt !== null ? 1 : 0, - 'page_len' => $len, - ); - - if ( $wgContentHandlerUseDB ) { - $row['page_content_model'] = $revision->getContentModel(); - } - - $dbw->update( 'page', - $row, - $conditions, - __METHOD__ ); - - $result = $dbw->affectedRows() > 0; - if ( $result ) { - $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); - $this->setLastEdit( $revision ); - $this->setCachedLastEditTime( $now ); - $this->mLatest = $revision->getId(); - $this->mIsRedirect = (bool)$rt; - // Update the LinkCache. - LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, - $this->mLatest, $revision->getContentModel() ); - } - - wfProfileOut( __METHOD__ ); - return $result; - } - - /** - * Add row to the redirect table if this is a redirect, remove otherwise. - * - * @param DatabaseBase $dbw - * @param Title $redirectTitle Title object pointing to the redirect target, - * or NULL if this is not a redirect - * @param null|bool $lastRevIsRedirect If given, will optimize adding and - * removing rows in redirect table. - * @return bool true on success, false on failure - * @private - */ - public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) { - // Always update redirects (target link might have changed) - // Update/Insert if we don't know if the last revision was a redirect or not - // Delete if changing from redirect to non-redirect - $isRedirect = !is_null( $redirectTitle ); - - if ( !$isRedirect && $lastRevIsRedirect === false ) { - return true; - } - - wfProfileIn( __METHOD__ ); - if ( $isRedirect ) { - $this->insertRedirectEntry( $redirectTitle ); - } else { - // This is not a redirect, remove row from redirect table - $where = array( 'rd_from' => $this->getId() ); - $dbw->delete( 'redirect', $where, __METHOD__ ); - } - - if ( $this->getTitle()->getNamespace() == NS_FILE ) { - RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); - } - wfProfileOut( __METHOD__ ); - - return ( $dbw->affectedRows() != 0 ); - } - - /** - * If the given revision is newer than the currently set page_latest, - * update the page record. Otherwise, do nothing. - * - * @deprecated since 1.24, use updateRevisionOn instead - * - * @param DatabaseBase $dbw - * @param Revision $revision - * @return bool - */ - public function updateIfNewerOn( $dbw, $revision ) { - wfProfileIn( __METHOD__ ); - - $row = $dbw->selectRow( - array( 'revision', 'page' ), - array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), - array( - 'page_id' => $this->getId(), - 'page_latest=rev_id' ), - __METHOD__ ); - - if ( $row ) { - if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) { - wfProfileOut( __METHOD__ ); - return false; - } - $prev = $row->rev_id; - $lastRevIsRedirect = (bool)$row->page_is_redirect; - } else { - // No or missing previous revision; mark the page as new - $prev = 0; - $lastRevIsRedirect = null; - } - - $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); - - wfProfileOut( __METHOD__ ); - return $ret; - } - - /** - * Get the content that needs to be saved in order to undo all revisions - * between $undo and $undoafter. Revisions must belong to the same page, - * must exist and must not be deleted - * @param Revision $undo - * @param Revision $undoafter Must be an earlier revision than $undo - * @return mixed string on success, false on failure - * @since 1.21 - * Before we had the Content object, this was done in getUndoText - */ - public function getUndoContent( Revision $undo, Revision $undoafter = null ) { - $handler = $undo->getContentHandler(); - return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter ); - } - - /** - * Get the text that needs to be saved in order to undo all revisions - * between $undo and $undoafter. Revisions must belong to the same page, - * must exist and must not be deleted - * @param Revision $undo - * @param Revision $undoafter Must be an earlier revision than $undo - * @return string|bool string on success, false on failure - * @deprecated since 1.21: use ContentHandler::getUndoContent() instead. - */ - public function getUndoText( Revision $undo, Revision $undoafter = null ) { - ContentHandler::deprecated( __METHOD__, '1.21' ); - - $this->loadLastEdit(); - - if ( $this->mLastRevision ) { - if ( is_null( $undoafter ) ) { - $undoafter = $undo->getPrevious(); - } - - $handler = $this->getContentHandler(); - $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter ); - - if ( !$undone ) { - return false; - } else { - return ContentHandler::getContentText( $undone ); - } - } - - return false; - } - - /** - * @param string|number|null|bool $sectionId Section identifier as a number or string - * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page - * or 'new' for a new section. - * @param string $text New text of the section. - * @param string $sectionTitle New section's subject, only if $section is "new". - * @param string $edittime Revision timestamp or null to use the current revision. - * - * @throws MWException - * @return string New complete article text, or null if error. - * - * @deprecated since 1.21, use replaceSectionAtRev() instead - */ - public function replaceSection( $sectionId, $text, $sectionTitle = '', - $edittime = null - ) { - ContentHandler::deprecated( __METHOD__, '1.21' ); - - //NOTE: keep condition in sync with condition in replaceSectionContent! - if ( strval( $sectionId ) === '' ) { - // Whole-page edit; let the whole text through - return $text; - } - - if ( !$this->supportsSections() ) { - throw new MWException( "sections not supported for content model " . - $this->getContentHandler()->getModelID() ); - } - - // could even make section title, but that's not required. - $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); - - $newContent = $this->replaceSectionContent( $sectionId, $sectionContent, $sectionTitle, - $edittime ); - - return ContentHandler::getContentText( $newContent ); - } - - /** - * Returns true if this page's content model supports sections. - * - * @return bool - * - * @todo The skin should check this and not offer section functionality if - * sections are not supported. - * @todo The EditPage should check this and not offer section functionality - * if sections are not supported. - */ - public function supportsSections() { - return $this->getContentHandler()->supportsSections(); - } - - /** - * @param string|number|null|bool $sectionId Section identifier as a number or string - * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page - * or 'new' for a new section. - * @param Content $sectionContent New content of the section. - * @param string $sectionTitle New section's subject, only if $section is "new". - * @param string $edittime Revision timestamp or null to use the current revision. - * - * @throws MWException - * @return Content New complete article content, or null if error. - * - * @since 1.21 - * @deprecated since 1.24, use replaceSectionAtRev instead - */ - public function replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle = '', - $edittime = null ) { - wfProfileIn( __METHOD__ ); - - $baseRevId = null; - if ( $edittime && $sectionId !== 'new' ) { - $dbw = wfGetDB( DB_MASTER ); - $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); - if ( $rev ) { - $baseRevId = $rev->getId(); - } - } - - wfProfileOut( __METHOD__ ); - return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId ); - } - - /** - * @param string|number|null|bool $sectionId Section identifier as a number or string - * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page - * or 'new' for a new section. - * @param Content $sectionContent New content of the section. - * @param string $sectionTitle New section's subject, only if $section is "new". - * @param string $baseRevId integer|null - * - * @throws MWException - * @return Content New complete article content, or null if error. - * - * @since 1.24 - */ - public function replaceSectionAtRev( $sectionId, Content $sectionContent, - $sectionTitle = '', $baseRevId = null - ) { - wfProfileIn( __METHOD__ ); - - if ( strval( $sectionId ) === '' ) { - // Whole-page edit; let the whole text through - $newContent = $sectionContent; - } else { - if ( !$this->supportsSections() ) { - wfProfileOut( __METHOD__ ); - throw new MWException( "sections not supported for content model " . - $this->getContentHandler()->getModelID() ); - } - - // Bug 30711: always use current version when adding a new section - if ( is_null( $baseRevId ) || $sectionId === 'new' ) { - $oldContent = $this->getContent(); - } else { - // TODO: try DB_SLAVE first - $dbw = wfGetDB( DB_MASTER ); - $rev = Revision::loadFromId( $dbw, $baseRevId ); - - if ( !$rev ) { - wfDebug( __METHOD__ . " asked for bogus section (page: " . - $this->getId() . "; section: $sectionId)\n" ); - wfProfileOut( __METHOD__ ); - return null; - } - - $oldContent = $rev->getContent(); - } - - if ( ! $oldContent ) { - wfDebug( __METHOD__ . ": no page text\n" ); - wfProfileOut( __METHOD__ ); - return null; - } - - $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle ); - } - - wfProfileOut( __METHOD__ ); - return $newContent; - } - - /** - * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed. - * @param int $flags - * @return int Updated $flags - */ - public function checkFlags( $flags ) { - if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) { - if ( $this->exists() ) { - $flags |= EDIT_UPDATE; - } else { - $flags |= EDIT_NEW; - } - } - - return $flags; - } - - /** - * Change an existing article or create a new article. Updates RC and all necessary caches, - * optionally via the deferred update array. - * - * @param string $text New text - * @param string $summary Edit summary - * @param int $flags Bitfield: - * EDIT_NEW - * Article is known or assumed to be non-existent, create a new one - * EDIT_UPDATE - * Article is known or assumed to be pre-existing, update it - * EDIT_MINOR - * Mark this edit minor, if the user is allowed to do so - * EDIT_SUPPRESS_RC - * Do not log the change in recentchanges - * EDIT_FORCE_BOT - * Mark the edit a "bot" edit regardless of user rights - * EDIT_DEFER_UPDATES - * Defer some of the updates until the end of index.php - * EDIT_AUTOSUMMARY - * Fill in blank summaries with generated text where possible - * - * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the - * article will be detected. If EDIT_UPDATE is specified and the article - * doesn't exist, the function will return an edit-gone-missing error. If - * EDIT_NEW is specified and the article does exist, an edit-already-exists - * error will be returned. These two conditions are also possible with - * auto-detection due to MediaWiki's performance-optimised locking strategy. - * - * @param bool|int $baseRevId The revision ID this edit was based off, if any - * @param User $user The user doing the edit - * - * @throws MWException - * @return Status object. Possible errors: - * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't - * set the fatal flag of $status - * edit-gone-missing: In update mode, but the article didn't exist. - * edit-conflict: In update mode, the article changed unexpectedly. - * edit-no-change: Warning that the text was the same as before. - * edit-already-exists: In creation mode, but the article already exists. - * - * Extensions may define additional errors. - * - * $return->value will contain an associative array with members as follows: - * new: Boolean indicating if the function attempted to create a new article. - * revision: The revision object for the inserted revision, or null. - * - * Compatibility note: this function previously returned a boolean value - * indicating success/failure - * - * @deprecated since 1.21: use doEditContent() instead. - */ - public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { - ContentHandler::deprecated( __METHOD__, '1.21' ); - - $content = ContentHandler::makeContent( $text, $this->getTitle() ); - - return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user ); - } - - /** - * Change an existing article or create a new article. Updates RC and all necessary caches, - * optionally via the deferred update array. - * - * @param Content $content New content - * @param string $summary Edit summary - * @param int $flags Bitfield: - * EDIT_NEW - * Article is known or assumed to be non-existent, create a new one - * EDIT_UPDATE - * Article is known or assumed to be pre-existing, update it - * EDIT_MINOR - * Mark this edit minor, if the user is allowed to do so - * EDIT_SUPPRESS_RC - * Do not log the change in recentchanges - * EDIT_FORCE_BOT - * Mark the edit a "bot" edit regardless of user rights - * EDIT_DEFER_UPDATES - * Defer some of the updates until the end of index.php - * EDIT_AUTOSUMMARY - * Fill in blank summaries with generated text where possible - * - * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the - * article will be detected. If EDIT_UPDATE is specified and the article - * doesn't exist, the function will return an edit-gone-missing error. If - * EDIT_NEW is specified and the article does exist, an edit-already-exists - * error will be returned. These two conditions are also possible with - * auto-detection due to MediaWiki's performance-optimised locking strategy. - * - * @param bool|int $baseRevId The revision ID this edit was based off, if any - * @param User $user The user doing the edit - * @param string $serialisation_format Format for storing the content in the - * database. - * - * @throws MWException - * @return Status object. Possible errors: - * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't - * set the fatal flag of $status. - * edit-gone-missing: In update mode, but the article didn't exist. - * edit-conflict: In update mode, the article changed unexpectedly. - * edit-no-change: Warning that the text was the same as before. - * edit-already-exists: In creation mode, but the article already exists. - * - * Extensions may define additional errors. - * - * $return->value will contain an associative array with members as follows: - * new: Boolean indicating if the function attempted to create a new article. - * revision: The revision object for the inserted revision, or null. - * - * @since 1.21 - */ - public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, - User $user = null, $serialisation_format = null - ) { - global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; - - // Low-level sanity check - if ( $this->mTitle->getText() === '' ) { - throw new MWException( 'Something is trying to edit an article with an empty title' ); - } - - wfProfileIn( __METHOD__ ); - - if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) { - wfProfileOut( __METHOD__ ); - return Status::newFatal( 'content-not-allowed-here', - ContentHandler::getLocalizedName( $content->getModel() ), - $this->getTitle()->getPrefixedText() ); - } - - $user = is_null( $user ) ? $wgUser : $user; - $status = Status::newGood( array() ); - - // Load the data from the master database if needed. - // The caller may already loaded it from the master or even loaded it using - // SELECT FOR UPDATE, so do not override that using clear(). - $this->loadPageData( 'fromdbmaster' ); - - $flags = $this->checkFlags( $flags ); - - // handle hook - $hook_args = array( &$this, &$user, &$content, &$summary, - $flags & EDIT_MINOR, null, null, &$flags, &$status ); - - if ( !wfRunHooks( 'PageContentSave', $hook_args ) - || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) { - - wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); - - if ( $status->isOK() ) { - $status->fatal( 'edit-hook-aborted' ); - } - - wfProfileOut( __METHOD__ ); - return $status; - } - - // Silently ignore EDIT_MINOR if not allowed - $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); - $bot = $flags & EDIT_FORCE_BOT; - - $old_content = $this->getContent( Revision::RAW ); // current revision's content - - $oldsize = $old_content ? $old_content->getSize() : 0; - $oldid = $this->getLatest(); - $oldIsRedirect = $this->isRedirect(); - $oldcountable = $this->isCountable(); - - $handler = $content->getContentHandler(); - - // Provide autosummaries if one is not provided and autosummaries are enabled. - if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { - if ( !$old_content ) { - $old_content = null; - } - $summary = $handler->getAutosummary( $old_content, $content, $flags ); - } - - $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); - $serialized = $editInfo->pst; - - /** - * @var Content $content - */ - $content = $editInfo->pstContent; - $newsize = $content->getSize(); - - $dbw = wfGetDB( DB_MASTER ); - $now = wfTimestampNow(); - $this->mTimestamp = $now; - - if ( $flags & EDIT_UPDATE ) { - // Update article, but only if changed. - $status->value['new'] = false; - - if ( !$oldid ) { - // Article gone missing - wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); - $status->fatal( 'edit-gone-missing' ); - - wfProfileOut( __METHOD__ ); - return $status; - } elseif ( !$old_content ) { - // Sanity check for bug 37225 - wfProfileOut( __METHOD__ ); - throw new MWException( "Could not find text for current revision {$oldid}." ); - } - - $revision = new Revision( array( - 'page' => $this->getId(), - 'title' => $this->getTitle(), // for determining the default content model - 'comment' => $summary, - 'minor_edit' => $isminor, - 'text' => $serialized, - 'len' => $newsize, - 'parent_id' => $oldid, - 'user' => $user->getId(), - 'user_text' => $user->getName(), - 'timestamp' => $now, - 'content_model' => $content->getModel(), - 'content_format' => $serialisation_format, - ) ); // XXX: pass content object?! - - $changed = !$content->equals( $old_content ); - - if ( $changed ) { - if ( !$content->isValid() ) { - wfProfileOut( __METHOD__ ); - throw new MWException( "New content failed validity check!" ); - } - - $dbw->begin( __METHOD__ ); - try { - - $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); - $status->merge( $prepStatus ); - - if ( !$status->isOK() ) { - $dbw->rollback( __METHOD__ ); - - wfProfileOut( __METHOD__ ); - return $status; - } - $revisionId = $revision->insertOn( $dbw ); - - // Update page - // - // We check for conflicts by comparing $oldid with the current latest revision ID. - $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); - - if ( !$ok ) { - // Belated edit conflict! Run away!! - $status->fatal( 'edit-conflict' ); - - $dbw->rollback( __METHOD__ ); - - wfProfileOut( __METHOD__ ); - return $status; - } - - wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); - // Update recentchanges - if ( !( $flags & EDIT_SUPPRESS_RC ) ) { - // Mark as patrolled if the user can do so - $patrolled = $wgUseRCPatrol && !count( - $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); - // Add RC row to the DB - $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, - $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, - $revisionId, $patrolled - ); - - // Log auto-patrolled edits - if ( $patrolled ) { - PatrolLog::record( $rc, true, $user ); - } - } - $user->incEditCount(); - } catch ( MWException $e ) { - $dbw->rollback( __METHOD__ ); - // Question: Would it perhaps be better if this method turned all - // exceptions into $status's? - throw $e; - } - $dbw->commit( __METHOD__ ); - } else { - // Bug 32948: revision ID must be set to page {{REVISIONID}} and - // related variables correctly - $revision->setId( $this->getLatest() ); - } - - // Update links tables, site stats, etc. - $this->doEditUpdates( - $revision, - $user, - array( - 'changed' => $changed, - 'oldcountable' => $oldcountable - ) - ); - - if ( !$changed ) { - $status->warning( 'edit-no-change' ); - $revision = null; - // Update page_touched, this is usually implicit in the page update - // Other cache updates are done in onArticleEdit() - $this->mTitle->invalidateCache(); - } - } else { - // Create new article - $status->value['new'] = true; - - $dbw->begin( __METHOD__ ); - try { - - $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); - $status->merge( $prepStatus ); - - if ( !$status->isOK() ) { - $dbw->rollback( __METHOD__ ); - - wfProfileOut( __METHOD__ ); - return $status; - } - - $status->merge( $prepStatus ); - - // Add the page record; stake our claim on this title! - // This will return false if the article already exists - $newid = $this->insertOn( $dbw ); - - if ( $newid === false ) { - $dbw->rollback( __METHOD__ ); - $status->fatal( 'edit-already-exists' ); - - wfProfileOut( __METHOD__ ); - return $status; - } - - // Save the revision text... - $revision = new Revision( array( - 'page' => $newid, - 'title' => $this->getTitle(), // for determining the default content model - 'comment' => $summary, - 'minor_edit' => $isminor, - 'text' => $serialized, - 'len' => $newsize, - 'user' => $user->getId(), - 'user_text' => $user->getName(), - 'timestamp' => $now, - 'content_model' => $content->getModel(), - 'content_format' => $serialisation_format, - ) ); - $revisionId = $revision->insertOn( $dbw ); - - // Bug 37225: use accessor to get the text as Revision may trim it - $content = $revision->getContent(); // sanity; get normalized version - - if ( $content ) { - $newsize = $content->getSize(); - } - - // Update the page record with revision data - $this->updateRevisionOn( $dbw, $revision, 0 ); - - wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); - - // Update recentchanges - if ( !( $flags & EDIT_SUPPRESS_RC ) ) { - // Mark as patrolled if the user can do so - $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( - $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); - // Add RC row to the DB - $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, - '', $newsize, $revisionId, $patrolled ); - - // Log auto-patrolled edits - if ( $patrolled ) { - PatrolLog::record( $rc, true, $user ); - } - } - $user->incEditCount(); - - } catch ( MWException $e ) { - $dbw->rollback( __METHOD__ ); - throw $e; - } - $dbw->commit( __METHOD__ ); - - // Update links, etc. - $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); - - $hook_args = array( &$this, &$user, $content, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision ); - - ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args ); - wfRunHooks( 'PageContentInsertComplete', $hook_args ); - } - - // Do updates right now unless deferral was requested - if ( !( $flags & EDIT_DEFER_UPDATES ) ) { - DeferredUpdates::doUpdates(); - } - - // Return the new revision (or null) to the caller - $status->value['revision'] = $revision; - - $hook_args = array( &$this, &$user, $content, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); - - ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); - wfRunHooks( 'PageContentSaveComplete', $hook_args ); - - // Promote user to any groups they meet the criteria for - $user->addAutopromoteOnceGroups( 'onEdit' ); - - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * Get parser options suitable for rendering the primary article wikitext - * - * @see ContentHandler::makeParserOptions - * - * @param IContextSource|User|string $context One of the following: - * - IContextSource: Use the User and the Language of the provided - * context - * - User: Use the provided User object and $wgLang for the language, - * so use an IContextSource object if possible. - * - 'canonical': Canonical options (anonymous user with default - * preferences and content language). - * @return ParserOptions - */ - public function makeParserOptions( $context ) { - $options = $this->getContentHandler()->makeParserOptions( $context ); - - if ( $this->getTitle()->isConversionTable() ) { - // @todo ConversionTable should become a separate content model, so - // we don't need special cases like this one. - $options->disableContentConversion(); - } - - return $options; - } - - /** - * Prepare text which is about to be saved. - * Returns a stdclass with source, pst and output members - * - * @deprecated since 1.21: use prepareContentForEdit instead. - * @return object - */ - public function prepareTextForEdit( $text, $revid = null, User $user = null ) { - ContentHandler::deprecated( __METHOD__, '1.21' ); - $content = ContentHandler::makeContent( $text, $this->getTitle() ); - return $this->prepareContentForEdit( $content, $revid, $user ); - } - - /** - * Prepare content which is about to be saved. - * Returns a stdclass with source, pst and output members - * - * @param Content $content - * @param int|null $revid - * @param User|null $user - * @param string|null $serialization_format - * - * @return bool|object - * - * @since 1.21 - */ - public function prepareContentForEdit( Content $content, $revid = null, User $user = null, - $serialization_format = null - ) { - global $wgContLang, $wgUser; - $user = is_null( $user ) ? $wgUser : $user; - //XXX: check $user->getId() here??? - - // Use a sane default for $serialization_format, see bug 57026 - if ( $serialization_format === null ) { - $serialization_format = $content->getContentHandler()->getDefaultFormat(); - } - - if ( $this->mPreparedEdit - && $this->mPreparedEdit->newContent - && $this->mPreparedEdit->newContent->equals( $content ) - && $this->mPreparedEdit->revid == $revid - && $this->mPreparedEdit->format == $serialization_format - // XXX: also check $user here? - ) { - // Already prepared - return $this->mPreparedEdit; - } - - $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); - wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) ); - - $edit = (object)array(); - $edit->revid = $revid; - $edit->timestamp = wfTimestampNow(); - - $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null; - - $edit->format = $serialization_format; - $edit->popts = $this->makeParserOptions( 'canonical' ); - $edit->output = $edit->pstContent - ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ) - : null; - - $edit->newContent = $content; - $edit->oldContent = $this->getContent( Revision::RAW ); - - // NOTE: B/C for hooks! don't use these fields! - $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : ''; - $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; - $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : ''; - - $this->mPreparedEdit = $edit; - return $edit; - } - - /** - * Do standard deferred updates after page edit. - * Update links tables, site stats, search index and message cache. - * Purges pages that include this page if the text was changed here. - * Every 100th edit, prune the recent changes table. - * - * @param Revision $revision - * @param User $user User object that did the revision - * @param array $options Array of options, following indexes are used: - * - changed: boolean, whether the revision changed the content (default true) - * - created: boolean, whether the revision created the page (default false) - * - oldcountable: boolean or null (default null): - * - boolean: whether the page was counted as an article before that - * revision, only used in changed is true and created is false - * - null: don't change the article count - */ - public function doEditUpdates( Revision $revision, User $user, array $options = array() ) { - global $wgEnableParserCache; - - wfProfileIn( __METHOD__ ); - - $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); - $content = $revision->getContent(); - - // Parse the text - // Be careful not to do pre-save transform twice: $text is usually - // already pre-save transformed once. - if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { - wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); - $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user ); - } else { - wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); - $editInfo = $this->mPreparedEdit; - } - - // Save it to the parser cache - if ( $wgEnableParserCache ) { - $parserCache = ParserCache::singleton(); - $parserCache->save( - $editInfo->output, $this, $editInfo->popts, $editInfo->timestamp, $editInfo->revid - ); - } - - // Update the links tables and other secondary data - if ( $content ) { - $recursive = $options['changed']; // bug 50785 - $updates = $content->getSecondaryDataUpdates( - $this->getTitle(), null, $recursive, $editInfo->output ); - DataUpdate::runUpdates( $updates ); - } - - wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); - - if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { - if ( 0 == mt_rand( 0, 99 ) ) { - // Flush old entries from the `recentchanges` table; we do this on - // random requests so as to avoid an increase in writes for no good reason - RecentChange::purgeExpiredChanges(); - } - } - - if ( !$this->exists() ) { - wfProfileOut( __METHOD__ ); - return; - } - - $id = $this->getId(); - $title = $this->mTitle->getPrefixedDBkey(); - $shortTitle = $this->mTitle->getDBkey(); - - if ( !$options['changed'] ) { - $good = 0; - } elseif ( $options['created'] ) { - $good = (int)$this->isCountable( $editInfo ); - } elseif ( $options['oldcountable'] !== null ) { - $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable']; - } else { - $good = 0; - } - $edits = $options['changed'] ? 1 : 0; - $total = $options['created'] ? 1 : 0; - - DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) ); - DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) ); - - // If this is another user's talk page, update newtalk. - // Don't do this if $options['changed'] = false (null-edits) nor if - // it's a minor edit and the user doesn't want notifications for those. - if ( $options['changed'] - && $this->mTitle->getNamespace() == NS_USER_TALK - && $shortTitle != $user->getTitleKey() - && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) ) - ) { - $recipient = User::newFromName( $shortTitle, false ); - if ( !$recipient ) { - wfDebug( __METHOD__ . ": invalid username\n" ); - } else { - // Allow extensions to prevent user notification when a new message is added to their talk page - if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) { - if ( User::isIP( $shortTitle ) ) { - // An anonymous user - $recipient->setNewtalk( true, $revision ); - } elseif ( $recipient->isLoggedIn() ) { - $recipient->setNewtalk( true, $revision ); - } else { - wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); - } - } - } - } - - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - // XXX: could skip pseudo-messages like js/css here, based on content model. - $msgtext = $content ? $content->getWikitextForTransclusion() : null; - if ( $msgtext === false || $msgtext === null ) { - $msgtext = ''; - } - - MessageCache::singleton()->replace( $shortTitle, $msgtext ); - } - - if ( $options['created'] ) { - self::onArticleCreate( $this->mTitle ); - } elseif ( $options['changed'] ) { // bug 50785 - self::onArticleEdit( $this->mTitle ); - } - - wfProfileOut( __METHOD__ ); - } - - /** - * Edit an article without doing all that other stuff - * The article must already exist; link tables etc - * are not updated, caches are not flushed. - * - * @param string $text Text submitted - * @param User $user The relevant user - * @param string $comment Comment submitted - * @param bool $minor Whereas it's a minor modification - * - * @deprecated since 1.21, use doEditContent() instead. - */ - public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { - ContentHandler::deprecated( __METHOD__, "1.21" ); - - $content = ContentHandler::makeContent( $text, $this->getTitle() ); - $this->doQuickEditContent( $content, $user, $comment, $minor ); - } - - /** - * Edit an article without doing all that other stuff - * The article must already exist; link tables etc - * are not updated, caches are not flushed. - * - * @param Content $content Content submitted - * @param User $user The relevant user - * @param string $comment comment submitted - * @param string $serialisation_format Format for storing the content in the database - * @param bool $minor Whereas it's a minor modification - */ - public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = false, - $serialisation_format = null - ) { - wfProfileIn( __METHOD__ ); - - $serialized = $content->serialize( $serialisation_format ); - - $dbw = wfGetDB( DB_MASTER ); - $revision = new Revision( array( - 'title' => $this->getTitle(), // for determining the default content model - 'page' => $this->getId(), - 'user_text' => $user->getName(), - 'user' => $user->getId(), - 'text' => $serialized, - 'length' => $content->getSize(), - 'comment' => $comment, - 'minor_edit' => $minor ? 1 : 0, - ) ); // XXX: set the content object? - $revision->insertOn( $dbw ); - $this->updateRevisionOn( $dbw, $revision ); - - wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); - - wfProfileOut( __METHOD__ ); - } - - /** - * Update the article's restriction field, and leave a log entry. - * This works for protection both existing and non-existing pages. - * - * @param array $limit Set of restriction keys - * @param array $expiry Per restriction type expiration - * @param int &$cascade Set to false if cascading protection isn't allowed. - * @param string $reason - * @param User $user The user updating the restrictions - * @return Status - */ - public function doUpdateRestrictions( array $limit, array $expiry, - &$cascade, $reason, User $user - ) { - global $wgCascadingRestrictionLevels, $wgContLang; - - if ( wfReadOnly() ) { - return Status::newFatal( 'readonlytext', wfReadOnlyReason() ); - } - - $this->loadPageData( 'fromdbmaster' ); - $restrictionTypes = $this->mTitle->getRestrictionTypes(); - $id = $this->getId(); - - if ( !$cascade ) { - $cascade = false; - } - - // Take this opportunity to purge out expired restrictions - Title::purgeExpiredRestrictions(); - - // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); - // we expect a single selection, but the schema allows otherwise. - $isProtected = false; - $protect = false; - $changed = false; - - $dbw = wfGetDB( DB_MASTER ); - - foreach ( $restrictionTypes as $action ) { - if ( !isset( $expiry[$action] ) ) { - $expiry[$action] = $dbw->getInfinity(); - } - if ( !isset( $limit[$action] ) ) { - $limit[$action] = ''; - } elseif ( $limit[$action] != '' ) { - $protect = true; - } - - // Get current restrictions on $action - $current = implode( '', $this->mTitle->getRestrictions( $action ) ); - if ( $current != '' ) { - $isProtected = true; - } - - if ( $limit[$action] != $current ) { - $changed = true; - } elseif ( $limit[$action] != '' ) { - // Only check expiry change if the action is actually being - // protected, since expiry does nothing on an not-protected - // action. - if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) { - $changed = true; - } - } - } - - if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) { - $changed = true; - } - - // If nothing has changed, do nothing - if ( !$changed ) { - return Status::newGood(); - } - - if ( !$protect ) { // No protection at all means unprotection - $revCommentMsg = 'unprotectedarticle'; - $logAction = 'unprotect'; - } elseif ( $isProtected ) { - $revCommentMsg = 'modifiedarticleprotection'; - $logAction = 'modify'; - } else { - $revCommentMsg = 'protectedarticle'; - $logAction = 'protect'; - } - - // Truncate for whole multibyte characters - $reason = $wgContLang->truncate( $reason, 255 ); - - $logRelationsValues = array(); - $logRelationsField = null; - - if ( $id ) { // Protection of existing page - if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) { - return Status::newGood(); - } - - // Only certain restrictions can cascade... - $editrestriction = isset( $limit['edit'] ) - ? array( $limit['edit'] ) - : $this->mTitle->getRestrictions( 'edit' ); - foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) { - $editrestriction[$key] = 'editprotected'; // backwards compatibility - } - foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) { - $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility - } - - $cascadingRestrictionLevels = $wgCascadingRestrictionLevels; - foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) { - $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility - } - foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) { - $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility - } - - // The schema allows multiple restrictions - if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) { - $cascade = false; - } - - // insert null revision to identify the page protection change as edit summary - $latest = $this->getLatest(); - $nullRevision = $this->insertProtectNullRevision( - $revCommentMsg, - $limit, - $expiry, - $cascade, - $reason, - $user - ); - - if ( $nullRevision === null ) { - return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() ); - } - - $logRelationsField = 'pr_id'; - - // Update restrictions table - foreach ( $limit as $action => $restrictions ) { - $dbw->delete( - 'page_restrictions', - array( - 'pr_page' => $id, - 'pr_type' => $action - ), - __METHOD__ - ); - if ( $restrictions != '' ) { - $dbw->insert( - 'page_restrictions', - array( - 'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ), - 'pr_page' => $id, - 'pr_type' => $action, - 'pr_level' => $restrictions, - 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0, - 'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] ) - ), - __METHOD__ - ); - $logRelationsValues[] = $dbw->insertId(); - } - } - - // Clear out legacy restriction fields - $dbw->update( - 'page', - array( 'page_restrictions' => '' ), - array( 'page_id' => $id ), - __METHOD__ - ); - - wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) ); - wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) ); - } else { // Protection of non-existing page (also known as "title protection") - // Cascade protection is meaningless in this case - $cascade = false; - - if ( $limit['create'] != '' ) { - $dbw->replace( 'protected_titles', - array( array( 'pt_namespace', 'pt_title' ) ), - array( - 'pt_namespace' => $this->mTitle->getNamespace(), - 'pt_title' => $this->mTitle->getDBkey(), - 'pt_create_perm' => $limit['create'], - 'pt_timestamp' => $dbw->timestamp(), - 'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ), - 'pt_user' => $user->getId(), - 'pt_reason' => $reason, - ), __METHOD__ - ); - } else { - $dbw->delete( 'protected_titles', - array( - 'pt_namespace' => $this->mTitle->getNamespace(), - 'pt_title' => $this->mTitle->getDBkey() - ), __METHOD__ - ); - } - } - - $this->mTitle->flushRestrictions(); - InfoAction::invalidateCache( $this->mTitle ); - - if ( $logAction == 'unprotect' ) { - $params = array(); - } else { - $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry ); - $params = array( $protectDescriptionLog, $cascade ? 'cascade' : '' ); - } - - // Update the protection log - $log = new LogPage( 'protect' ); - $logId = $log->addEntry( $logAction, $this->mTitle, $reason, $params, $user ); - if ( $logRelationsField !== null && count( $logRelationsValues ) ) { - $log->addRelations( $logRelationsField, $logRelationsValues, $logId ); - } - - return Status::newGood(); - } - - /** - * Insert a new null revision for this page. - * - * @param string $revCommentMsg Comment message key for the revision - * @param array $limit Set of restriction keys - * @param array $expiry Per restriction type expiration - * @param int $cascade Set to false if cascading protection isn't allowed. - * @param string $reason - * @param User|null $user - * @return Revision|null Null on error - */ - public function insertProtectNullRevision( $revCommentMsg, array $limit, - array $expiry, $cascade, $reason, $user = null - ) { - global $wgContLang; - $dbw = wfGetDB( DB_MASTER ); - - // Prepare a null revision to be added to the history - $editComment = $wgContLang->ucfirst( - wfMessage( - $revCommentMsg, - $this->mTitle->getPrefixedText() - )->inContentLanguage()->text() - ); - if ( $reason ) { - $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; - } - $protectDescription = $this->protectDescription( $limit, $expiry ); - if ( $protectDescription ) { - $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); - $editComment .= wfMessage( 'parentheses' )->params( $protectDescription ) - ->inContentLanguage()->text(); - } - if ( $cascade ) { - $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); - $editComment .= wfMessage( 'brackets' )->params( - wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text() - )->inContentLanguage()->text(); - } - - $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user ); - if ( $nullRev ) { - $nullRev->insertOn( $dbw ); - - // Update page record and touch page - $oldLatest = $nullRev->getParentId(); - $this->updateRevisionOn( $dbw, $nullRev, $oldLatest ); - } - - return $nullRev; - } - - /** - * @param string $expiry 14-char timestamp or "infinity", or false if the input was invalid - * @return string - */ - protected function formatExpiry( $expiry ) { - global $wgContLang; - $dbr = wfGetDB( DB_SLAVE ); - - $encodedExpiry = $dbr->encodeExpiry( $expiry ); - if ( $encodedExpiry != 'infinity' ) { - return wfMessage( - 'protect-expiring', - $wgContLang->timeanddate( $expiry, false, false ), - $wgContLang->date( $expiry, false, false ), - $wgContLang->time( $expiry, false, false ) - )->inContentLanguage()->text(); - } else { - return wfMessage( 'protect-expiry-indefinite' ) - ->inContentLanguage()->text(); - } - } - - /** - * Builds the description to serve as comment for the edit. - * - * @param array $limit Set of restriction keys - * @param array $expiry Per restriction type expiration - * @return string - */ - public function protectDescription( array $limit, array $expiry ) { - $protectDescription = ''; - - foreach ( array_filter( $limit ) as $action => $restrictions ) { - # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ). - # All possible message keys are listed here for easier grepping: - # * restriction-create - # * restriction-edit - # * restriction-move - # * restriction-upload - $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text(); - # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ), - # with '' filtered out. All possible message keys are listed below: - # * protect-level-autoconfirmed - # * protect-level-sysop - $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text(); - - $expiryText = $this->formatExpiry( $expiry[$action] ); - - if ( $protectDescription !== '' ) { - $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text(); - } - $protectDescription .= wfMessage( 'protect-summary-desc' ) - ->params( $actionText, $restrictionsText, $expiryText ) - ->inContentLanguage()->text(); - } - - return $protectDescription; - } - - /** - * Builds the description to serve as comment for the log entry. - * - * Some bots may parse IRC lines, which are generated from log entries which contain plain - * protect description text. Keep them in old format to avoid breaking compatibility. - * TODO: Fix protection log to store structured description and format it on-the-fly. - * - * @param array $limit Set of restriction keys - * @param array $expiry Per restriction type expiration - * @return string - */ - public function protectDescriptionLog( array $limit, array $expiry ) { - global $wgContLang; - - $protectDescriptionLog = ''; - - foreach ( array_filter( $limit ) as $action => $restrictions ) { - $expiryText = $this->formatExpiry( $expiry[$action] ); - $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ($expiryText)"; - } - - return trim( $protectDescriptionLog ); - } - - /** - * Take an array of page restrictions and flatten it to a string - * suitable for insertion into the page_restrictions field. - * - * @param string[] $limit - * - * @throws MWException - * @return string - */ - protected static function flattenRestrictions( $limit ) { - if ( !is_array( $limit ) ) { - throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' ); - } - - $bits = array(); - ksort( $limit ); - - foreach ( array_filter( $limit ) as $action => $restrictions ) { - $bits[] = "$action=$restrictions"; - } - - return implode( ':', $bits ); - } - - /** - * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for - * backwards compatibility, if you care about error reporting you should use - * doDeleteArticleReal() instead. - * - * Deletes the article with database consistency, writes logs, purges caches - * - * @param string $reason Delete reason for deletion log - * @param bool $suppress Suppress all revisions and log the deletion in - * the suppression log instead of the deletion log - * @param int $id Article ID - * @param bool $commit Defaults to true, triggers transaction end - * @param array &$error Array of errors to append to - * @param User $user The deleting user - * @return bool true if successful - */ - public function doDeleteArticle( - $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null - ) { - $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user ); - return $status->isGood(); - } - - /** - * Back-end article deletion - * Deletes the article with database consistency, writes logs, purges caches - * - * @since 1.19 - * - * @param string $reason Delete reason for deletion log - * @param bool $suppress Suppress all revisions and log the deletion in - * the suppression log instead of the deletion log - * @param int $id Article ID - * @param bool $commit Defaults to true, triggers transaction end - * @param array &$error Array of errors to append to - * @param User $user The deleting user - * @return Status Status object; if successful, $status->value is the log_id of the - * deletion log entry. If the page couldn't be deleted because it wasn't - * found, $status is a non-fatal 'cannotdelete' error - */ - public function doDeleteArticleReal( - $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null - ) { - global $wgUser, $wgContentHandlerUseDB; - - wfDebug( __METHOD__ . "\n" ); - - $status = Status::newGood(); - - if ( $this->mTitle->getDBkey() === '' ) { - $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); - return $status; - } - - $user = is_null( $user ) ? $wgUser : $user; - if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) { - if ( $status->isOK() ) { - // Hook aborted but didn't set a fatal status - $status->fatal( 'delete-hook-aborted' ); - } - return $status; - } - - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin( __METHOD__ ); - - if ( $id == 0 ) { - $this->loadPageData( 'forupdate' ); - $id = $this->getID(); - if ( $id == 0 ) { - $dbw->rollback( __METHOD__ ); - $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); - return $status; - } - } - - // we need to remember the old content so we can use it to generate all deletion updates. - $content = $this->getContent( Revision::RAW ); - - // Bitfields to further suppress the content - if ( $suppress ) { - $bitfield = 0; - // This should be 15... - $bitfield |= Revision::DELETED_TEXT; - $bitfield |= Revision::DELETED_COMMENT; - $bitfield |= Revision::DELETED_USER; - $bitfield |= Revision::DELETED_RESTRICTED; - } else { - $bitfield = 'rev_deleted'; - } - - // For now, shunt the revision data into the archive table. - // Text is *not* removed from the text table; bulk storage - // is left intact to avoid breaking block-compression or - // immutable storage schemes. - // - // For backwards compatibility, note that some older archive - // table entries will have ar_text and ar_flags fields still. - // - // In the future, we may keep revisions and mark them with - // the rev_deleted field, which is reserved for this purpose. - - $row = array( - 'ar_namespace' => 'page_namespace', - 'ar_title' => 'page_title', - 'ar_comment' => 'rev_comment', - 'ar_user' => 'rev_user', - 'ar_user_text' => 'rev_user_text', - 'ar_timestamp' => 'rev_timestamp', - 'ar_minor_edit' => 'rev_minor_edit', - 'ar_rev_id' => 'rev_id', - 'ar_parent_id' => 'rev_parent_id', - 'ar_text_id' => 'rev_text_id', - 'ar_text' => '\'\'', // Be explicit to appease - 'ar_flags' => '\'\'', // MySQL's "strict mode"... - 'ar_len' => 'rev_len', - 'ar_page_id' => 'page_id', - 'ar_deleted' => $bitfield, - 'ar_sha1' => 'rev_sha1', - ); - - if ( $wgContentHandlerUseDB ) { - $row['ar_content_model'] = 'rev_content_model'; - $row['ar_content_format'] = 'rev_content_format'; - } - - $dbw->insertSelect( 'archive', array( 'page', 'revision' ), - $row, - array( - 'page_id' => $id, - 'page_id = rev_page' - ), __METHOD__ - ); - - // Now that it's safely backed up, delete it - $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); - $ok = ( $dbw->affectedRows() > 0 ); // $id could be laggy - - if ( !$ok ) { - $dbw->rollback( __METHOD__ ); - $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); - return $status; - } - - if ( !$dbw->cascadingDeletes() ) { - $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); - } - - // Clone the title, so we have the information we need when we log - $logTitle = clone $this->mTitle; - - $this->doDeleteUpdates( $id, $content ); - - // Log the deletion, if the page was suppressed, log it at Oversight instead - $logtype = $suppress ? 'suppress' : 'delete'; - - $logEntry = new ManualLogEntry( $logtype, 'delete' ); - $logEntry->setPerformer( $user ); - $logEntry->setTarget( $logTitle ); - $logEntry->setComment( $reason ); - $logid = $logEntry->insert(); - - $dbw->onTransactionPreCommitOrIdle( function() use ( $dbw, $logEntry, $logid ) { - // Bug 56776: avoid deadlocks (especially from FileDeleteForm) - $logEntry->publish( $logid ); - } ); - - if ( $commit ) { - $dbw->commit( __METHOD__ ); - } - - wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) ); - $status->value = $logid; - return $status; - } - - /** - * Do some database updates after deletion - * - * @param int $id page_id value of the page being deleted - * @param Content $content Optional page content to be used when determining - * the required updates. This may be needed because $this->getContent() - * may already return null when the page proper was deleted. - */ - public function doDeleteUpdates( $id, Content $content = null ) { - // update site status - DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); - - // remove secondary indexes, etc - $updates = $this->getDeletionUpdates( $content ); - DataUpdate::runUpdates( $updates ); - - // Reparse any pages transcluding this page - LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' ); - - // Reparse any pages including this image - if ( $this->mTitle->getNamespace() == NS_FILE ) { - LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' ); - } - - // Clear caches - WikiPage::onArticleDelete( $this->mTitle ); - - // Reset this object and the Title object - $this->loadFromRow( false, self::READ_LATEST ); - - // Search engine - DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) ); - } - - /** - * Roll back the most recent consecutive set of edits to a page - * from the same user; fails if there are no eligible edits to - * roll back to, e.g. user is the sole contributor. This function - * performs permissions checks on $user, then calls commitRollback() - * to do the dirty work - * - * @todo Separate the business/permission stuff out from backend code - * - * @param string $fromP Name of the user whose edits to rollback. - * @param string $summary Custom summary. Set to default summary if empty. - * @param string $token Rollback token. - * @param bool $bot If true, mark all reverted edits as bot. - * - * @param array $resultDetails contains result-specific array of additional values - * 'alreadyrolled' : 'current' (rev) - * success : 'summary' (str), 'current' (rev), 'target' (rev) - * - * @param User $user The user performing the rollback - * @return array Array of errors, each error formatted as - * array(messagekey, param1, param2, ...). - * On success, the array is empty. This array can also be passed to - * OutputPage::showPermissionsErrorPage(). - */ - public function doRollback( - $fromP, $summary, $token, $bot, &$resultDetails, User $user - ) { - $resultDetails = null; - - // Check permissions - $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user ); - $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user ); - $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); - - if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) { - $errors[] = array( 'sessionfailure' ); - } - - if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) { - $errors[] = array( 'actionthrottledtext' ); - } - - // If there were errors, bail out now - if ( !empty( $errors ) ) { - return $errors; - } - - return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user ); - } - - /** - * Backend implementation of doRollback(), please refer there for parameter - * and return value documentation - * - * NOTE: This function does NOT check ANY permissions, it just commits the - * rollback to the DB. Therefore, you should only call this function direct- - * ly if you want to use custom permissions checks. If you don't, use - * doRollback() instead. - * @param string $fromP Name of the user whose edits to rollback. - * @param string $summary Custom summary. Set to default summary if empty. - * @param bool $bot If true, mark all reverted edits as bot. - * - * @param array $resultDetails Contains result-specific array of additional values - * @param User $guser The user performing the rollback - * @return array - */ - public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) { - global $wgUseRCPatrol, $wgContLang; - - $dbw = wfGetDB( DB_MASTER ); - - if ( wfReadOnly() ) { - return array( array( 'readonlytext' ) ); - } - - // Get the last editor - $current = $this->getRevision(); - if ( is_null( $current ) ) { - // Something wrong... no page? - return array( array( 'notanarticle' ) ); - } - - $from = str_replace( '_', ' ', $fromP ); - // User name given should match up with the top revision. - // If the user was deleted then $from should be empty. - if ( $from != $current->getUserText() ) { - $resultDetails = array( 'current' => $current ); - return array( array( 'alreadyrolled', - htmlspecialchars( $this->mTitle->getPrefixedText() ), - htmlspecialchars( $fromP ), - htmlspecialchars( $current->getUserText() ) - ) ); - } - - // Get the last edit not by this guy... - // Note: these may not be public values - $user = intval( $current->getRawUser() ); - $user_text = $dbw->addQuotes( $current->getRawUserText() ); - $s = $dbw->selectRow( 'revision', - array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), - array( 'rev_page' => $current->getPage(), - "rev_user != {$user} OR rev_user_text != {$user_text}" - ), __METHOD__, - array( 'USE INDEX' => 'page_timestamp', - 'ORDER BY' => 'rev_timestamp DESC' ) - ); - if ( $s === false ) { - // No one else ever edited this page - return array( array( 'cantrollback' ) ); - } elseif ( $s->rev_deleted & Revision::DELETED_TEXT - || $s->rev_deleted & Revision::DELETED_USER - ) { - // Only admins can see this text - return array( array( 'notvisiblerev' ) ); - } - - // Set patrolling and bot flag on the edits, which gets rollbacked. - // This is done before the rollback edit to have patrolling also on failure (bug 62157). - $set = array(); - if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { - // Mark all reverted edits as bot - $set['rc_bot'] = 1; - } - - if ( $wgUseRCPatrol ) { - // Mark all reverted edits as patrolled - $set['rc_patrolled'] = 1; - } - - if ( count( $set ) ) { - $dbw->update( 'recentchanges', $set, - array( /* WHERE */ - 'rc_cur_id' => $current->getPage(), - 'rc_user_text' => $current->getUserText(), - 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ), - ), __METHOD__ - ); - } - - // Generate the edit summary if necessary - $target = Revision::newFromId( $s->rev_id ); - if ( empty( $summary ) ) { - if ( $from == '' ) { // no public user name - $summary = wfMessage( 'revertpage-nouser' ); - } else { - $summary = wfMessage( 'revertpage' ); - } - } - - // Allow the custom summary to use the same args as the default message - $args = array( - $target->getUserText(), $from, $s->rev_id, - $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ), - $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() ) - ); - if ( $summary instanceof Message ) { - $summary = $summary->params( $args )->inContentLanguage()->text(); - } else { - $summary = wfMsgReplaceArgs( $summary, $args ); - } - - // Trim spaces on user supplied text - $summary = trim( $summary ); - - // Truncate for whole multibyte characters. - $summary = $wgContLang->truncate( $summary, 255 ); - - // Save - $flags = EDIT_UPDATE; - - if ( $guser->isAllowed( 'minoredit' ) ) { - $flags |= EDIT_MINOR; - } - - if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) { - $flags |= EDIT_FORCE_BOT; - } - - // Actually store the edit - $status = $this->doEditContent( - $target->getContent(), - $summary, - $flags, - $target->getId(), - $guser - ); - - if ( !$status->isOK() ) { - return $status->getErrorsArray(); - } - - // raise error, when the edit is an edit without a new version - if ( empty( $status->value['revision'] ) ) { - $resultDetails = array( 'current' => $current ); - return array( array( 'alreadyrolled', - htmlspecialchars( $this->mTitle->getPrefixedText() ), - htmlspecialchars( $fromP ), - htmlspecialchars( $current->getUserText() ) - ) ); - } - - $revId = $status->value['revision']->getId(); - - wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) ); - - $resultDetails = array( - 'summary' => $summary, - 'current' => $current, - 'target' => $target, - 'newid' => $revId - ); - - return array(); - } - - /** - * The onArticle*() functions are supposed to be a kind of hooks - * which should be called whenever any of the specified actions - * are done. - * - * This is a good place to put code to clear caches, for instance. - * - * This is called on page move and undelete, as well as edit - * - * @param Title $title - */ - public static function onArticleCreate( $title ) { - // Update existence markers on article/talk tabs... - if ( $title->isTalkPage() ) { - $other = $title->getSubjectPage(); - } else { - $other = $title->getTalkPage(); - } - - $other->invalidateCache(); - $other->purgeSquid(); - - $title->touchLinks(); - $title->purgeSquid(); - $title->deleteTitleProtection(); - } - - /** - * Clears caches when article is deleted - * - * @param Title $title - */ - public static function onArticleDelete( $title ) { - // Update existence markers on article/talk tabs... - if ( $title->isTalkPage() ) { - $other = $title->getSubjectPage(); - } else { - $other = $title->getTalkPage(); - } - - $other->invalidateCache(); - $other->purgeSquid(); - - $title->touchLinks(); - $title->purgeSquid(); - - // File cache - HTMLFileCache::clearFileCache( $title ); - InfoAction::invalidateCache( $title ); - - // Messages - if ( $title->getNamespace() == NS_MEDIAWIKI ) { - MessageCache::singleton()->replace( $title->getDBkey(), false ); - } - - // Images - if ( $title->getNamespace() == NS_FILE ) { - $update = new HTMLCacheUpdate( $title, 'imagelinks' ); - $update->doUpdate(); - } - - // User talk pages - if ( $title->getNamespace() == NS_USER_TALK ) { - $user = User::newFromName( $title->getText(), false ); - if ( $user ) { - $user->setNewtalk( false ); - } - } - - // Image redirects - RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); - } - - /** - * Purge caches on page update etc - * - * @param Title $title - * @todo Verify that $title is always a Title object (and never false or - * null), add Title hint to parameter $title. - */ - public static function onArticleEdit( $title ) { - // Invalidate caches of articles which include this page - DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' ); - - // Invalidate the caches of all pages which redirect here - DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' ); - - // Purge squid for this page only - $title->purgeSquid(); - - // Clear file cache for this page only - HTMLFileCache::clearFileCache( $title ); - InfoAction::invalidateCache( $title ); - } - - /**#@-*/ - - /** - * Returns a list of categories this page is a member of. - * Results will include hidden categories - * - * @return TitleArray - */ - public function getCategories() { - $id = $this->getId(); - if ( $id == 0 ) { - return TitleArray::newFromResult( new FakeResultWrapper( array() ) ); - } - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'categorylinks', - array( 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ), - // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes - // as not being aliases, and NS_CATEGORY is numeric - array( 'cl_from' => $id ), - __METHOD__ ); - - return TitleArray::newFromResult( $res ); - } - - /** - * Returns a list of hidden categories this page is a member of. - * Uses the page_props and categorylinks tables. - * - * @return array Array of Title objects - */ - public function getHiddenCategories() { - $result = array(); - $id = $this->getId(); - - if ( $id == 0 ) { - return array(); - } - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), - array( 'cl_to' ), - array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', - 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ), - __METHOD__ ); - - if ( $res !== false ) { - foreach ( $res as $row ) { - $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); - } - } - - return $result; - } - - /** - * Return an applicable autosummary if one exists for the given edit. - * @param string|null $oldtext The previous text of the page. - * @param string|null $newtext The submitted text of the page. - * @param int $flags Bitmask: a bitmask of flags submitted for the edit. - * @return string An appropriate autosummary, or an empty string. - * - * @deprecated since 1.21, use ContentHandler::getAutosummary() instead - */ - public static function getAutosummary( $oldtext, $newtext, $flags ) { - // NOTE: stub for backwards-compatibility. assumes the given text is - // wikitext. will break horribly if it isn't. - - ContentHandler::deprecated( __METHOD__, '1.21' ); - - $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); - $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext ); - $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext ); - - return $handler->getAutosummary( $oldContent, $newContent, $flags ); - } - - /** - * Auto-generates a deletion reason - * - * @param bool &$hasHistory Whether the page has a history - * @return string|bool String containing deletion reason or empty string, or boolean false - * if no revision occurred - */ - public function getAutoDeleteReason( &$hasHistory ) { - return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory ); - } - - /** - * Update all the appropriate counts in the category table, given that - * we've added the categories $added and deleted the categories $deleted. - * - * @param array $added The names of categories that were added - * @param array $deleted The names of categories that were deleted - */ - public function updateCategoryCounts( array $added, array $deleted ) { - $that = $this; - $method = __METHOD__; - $dbw = wfGetDB( DB_MASTER ); - - // Do this at the end of the commit to reduce lock wait timeouts - $dbw->onTransactionPreCommitOrIdle( - function() use ( $dbw, $that, $method, $added, $deleted ) { - $ns = $that->getTitle()->getNamespace(); - - $addFields = array( 'cat_pages = cat_pages + 1' ); - $removeFields = array( 'cat_pages = cat_pages - 1' ); - if ( $ns == NS_CATEGORY ) { - $addFields[] = 'cat_subcats = cat_subcats + 1'; - $removeFields[] = 'cat_subcats = cat_subcats - 1'; - } elseif ( $ns == NS_FILE ) { - $addFields[] = 'cat_files = cat_files + 1'; - $removeFields[] = 'cat_files = cat_files - 1'; - } - - if ( count( $added ) ) { - $insertRows = array(); - foreach ( $added as $cat ) { - $insertRows[] = array( - 'cat_title' => $cat, - 'cat_pages' => 1, - 'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0, - 'cat_files' => ( $ns == NS_FILE ) ? 1 : 0, - ); - } - $dbw->upsert( - 'category', - $insertRows, - array( 'cat_title' ), - $addFields, - $method - ); - } - - if ( count( $deleted ) ) { - $dbw->update( - 'category', - $removeFields, - array( 'cat_title' => $deleted ), - $method - ); - } - - foreach ( $added as $catName ) { - $cat = Category::newFromName( $catName ); - wfRunHooks( 'CategoryAfterPageAdded', array( $cat, $that ) ); - } - - foreach ( $deleted as $catName ) { - $cat = Category::newFromName( $catName ); - wfRunHooks( 'CategoryAfterPageRemoved', array( $cat, $that ) ); - } - } - ); - } - - /** - * Updates cascading protections - * - * @param ParserOutput $parserOutput ParserOutput object for the current version - */ - public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) { - if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { - return; - } - - // templatelinks or imagelinks tables may have become out of sync, - // especially if using variable-based transclusions. - // For paranoia, check if things have changed and if - // so apply updates to the database. This will ensure - // that cascaded protections apply as soon as the changes - // are visible. - - // Get templates from templatelinks and images from imagelinks - $id = $this->getId(); - - $dbLinks = array(); - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'templatelinks' ), - array( 'tl_namespace', 'tl_title' ), - array( 'tl_from' => $id ), - __METHOD__ - ); - - foreach ( $res as $row ) { - $dbLinks["{$row->tl_namespace}:{$row->tl_title}"] = true; - } - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'imagelinks' ), - array( 'il_to' ), - array( 'il_from' => $id ), - __METHOD__ - ); - - foreach ( $res as $row ) { - $dbLinks[NS_FILE . ":{$row->il_to}"] = true; - } - - // Get templates and images from parser output. - $poLinks = array(); - foreach ( $parserOutput->getTemplates() as $ns => $templates ) { - foreach ( $templates as $dbk => $id ) { - $poLinks["$ns:$dbk"] = true; - } - } - foreach ( $parserOutput->getImages() as $dbk => $id ) { - $poLinks[NS_FILE . ":$dbk"] = true; - } - - // Get the diff - $links_diff = array_diff_key( $poLinks, $dbLinks ); - - if ( count( $links_diff ) > 0 ) { - // Whee, link updates time. - // Note: we are only interested in links here. We don't need to get - // other DataUpdate items from the parser output. - $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); - $u->doUpdate(); - } - } - - /** - * Return a list of templates used by this article. - * Uses the templatelinks table - * - * @deprecated since 1.19; use Title::getTemplateLinksFrom() - * @return array Array of Title objects - */ - public function getUsedTemplates() { - return $this->mTitle->getTemplateLinksFrom(); - } - - /** - * This function is called right before saving the wikitext, - * so we can do things like signatures and links-in-context. - * - * @deprecated since 1.19; use Parser::preSaveTransform() instead - * @param string $text Article contents - * @param User $user User doing the edit - * @param ParserOptions $popts Parser options, default options for - * the user loaded if null given - * @return string Article contents with altered wikitext markup (signatures - * converted, {{subst:}}, templates, etc.) - */ - public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) { - global $wgParser, $wgUser; - - wfDeprecated( __METHOD__, '1.19' ); - - $user = is_null( $user ) ? $wgUser : $user; - - if ( $popts === null ) { - $popts = ParserOptions::newFromUser( $user ); - } - - return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); - } - - /** - * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit - * - * @deprecated since 1.19; use Title::isBigDeletion() instead. - * @return bool - */ - public function isBigDeletion() { - wfDeprecated( __METHOD__, '1.19' ); - return $this->mTitle->isBigDeletion(); - } - - /** - * Get the approximate revision count of this page. - * - * @deprecated since 1.19; use Title::estimateRevisionCount() instead. - * @return int - */ - public function estimateRevisionCount() { - wfDeprecated( __METHOD__, '1.19' ); - return $this->mTitle->estimateRevisionCount(); - } - - /** - * Update the article's restriction field, and leave a log entry. - * - * @deprecated since 1.19 - * @param array $limit Set of restriction keys - * @param string $reason - * @param int &$cascade Set to false if cascading protection isn't allowed. - * @param array $expiry Per restriction type expiration - * @param User $user The user updating the restrictions - * @return bool true on success - */ - public function updateRestrictions( - $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null - ) { - global $wgUser; - - $user = is_null( $user ) ? $wgUser : $user; - - return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK(); - } - - /** - * Returns a list of updates to be performed when this page is deleted. The - * updates should remove any information about this page from secondary data - * stores such as links tables. - * - * @param Content|null $content Optional Content object for determining the - * necessary updates. - * @return array An array of DataUpdates objects - */ - public function getDeletionUpdates( Content $content = null ) { - if ( !$content ) { - // load content object, which may be used to determine the necessary updates - // XXX: the content may not be needed to determine the updates, then this would be overhead. - $content = $this->getContent( Revision::RAW ); - } - - if ( !$content ) { - $updates = array(); - } else { - $updates = $content->getDeletionUpdates( $this ); - } - - wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) ); - return $updates; - } - -} - -class PoolWorkArticleView extends PoolCounterWork { - /** @var Page */ - private $page; - - /** @var string */ - private $cacheKey; - - /** @var int */ - private $revid; - - /** @var ParserOptions */ - private $parserOptions; - - /** @var Content|null */ - private $content = null; - - /** @var ParserOutput|bool */ - private $parserOutput = false; - - /** @var bool */ - private $isDirty = false; - - /** @var Status|bool */ - private $error = false; - - /** - * @param Page $page - * @param int $revid ID of the revision being parsed. - * @param bool $useParserCache Whether to use the parser cache. - * @param ParserOptions $parserOptions ParserOptions to use for the parse - * operation. - * @param Content|string $content Content to parse or null to load it; may - * also be given as a wikitext string, for BC. - */ - public function __construct( Page $page, ParserOptions $parserOptions, - $revid, $useParserCache, $content = null - ) { - if ( is_string( $content ) ) { // BC: old style call - $modelId = $page->getRevision()->getContentModel(); - $format = $page->getRevision()->getContentFormat(); - $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format ); - } - - $this->page = $page; - $this->revid = $revid; - $this->cacheable = $useParserCache; - $this->parserOptions = $parserOptions; - $this->content = $content; - $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); - parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); - } - - /** - * Get the ParserOutput from this object, or false in case of failure - * - * @return ParserOutput - */ - public function getParserOutput() { - return $this->parserOutput; - } - - /** - * Get whether the ParserOutput is a dirty one (i.e. expired) - * - * @return bool - */ - public function getIsDirty() { - return $this->isDirty; - } - - /** - * Get a Status object in case of error or false otherwise - * - * @return Status|bool - */ - public function getError() { - return $this->error; - } - - /** - * @return bool - */ - public function doWork() { - global $wgUseFileCache; - - // @todo several of the methods called on $this->page are not declared in Page, but present - // in WikiPage and delegated by Article. - - $isCurrent = $this->revid === $this->page->getLatest(); - - if ( $this->content !== null ) { - $content = $this->content; - } elseif ( $isCurrent ) { - // XXX: why use RAW audience here, and PUBLIC (default) below? - $content = $this->page->getContent( Revision::RAW ); - } else { - $rev = Revision::newFromTitle( $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 ( $content === null ) { - return false; - } - - // Reduce effects of race conditions for slow parses (bug 46014) - $cacheTime = wfTimestampNow(); - - $time = - microtime( true ); - $this->parserOutput = $content->getParserOutput( - $this->page->getTitle(), - $this->revid, - $this->parserOptions - ); - $time += microtime( true ); - - // Timing hack - if ( $time > 3 ) { - wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, - $this->page->getTitle()->getPrefixedDBkey() ) ); - } - - if ( $this->cacheable && $this->parserOutput->isCacheable() && $isCurrent ) { - ParserCache::singleton()->save( - $this->parserOutput, $this->page, $this->parserOptions, $cacheTime, $this->revid ); - } - - // Make sure file cache is not used on uncacheable content. - // Output that has magic words in it can still use the parser cache - // (if enabled), though it will generally expire sooner. - if ( !$this->parserOutput->isCacheable() || $this->parserOutput->containsOldMagic() ) { - $wgUseFileCache = false; - } - - if ( $isCurrent ) { - $this->page->doCascadeProtectionUpdates( $this->parserOutput ); - } - - return true; - } - - /** - * @return bool - */ - public function getCachedWork() { - $this->parserOutput = ParserCache::singleton()->get( $this->page, $this->parserOptions ); - - if ( $this->parserOutput === false ) { - wfDebug( __METHOD__ . ": parser cache miss\n" ); - return false; - } else { - wfDebug( __METHOD__ . ": parser cache hit\n" ); - return true; - } - } - - /** - * @return bool - */ - public function fallback() { - $this->parserOutput = ParserCache::singleton()->getDirty( $this->page, $this->parserOptions ); - - if ( $this->parserOutput === false ) { - wfDebugLog( 'dirty', 'dirty missing' ); - wfDebug( __METHOD__ . ": no dirty cache\n" ); - return false; - } else { - wfDebug( __METHOD__ . ": sending dirty output\n" ); - wfDebugLog( 'dirty', "dirty output {$this->cacheKey}" ); - $this->isDirty = true; - return true; - } - } - - /** - * @param Status $status - * @return bool - */ - public function error( $status ) { - $this->error = $status; - return false; - } -} diff --git a/includes/page/Article.php b/includes/page/Article.php new file mode 100644 index 0000000000..c68c675dc9 --- /dev/null +++ b/includes/page/Article.php @@ -0,0 +1,2100 @@ +<?php +/** + * User interface for page actions. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Class for viewing MediaWiki article and history. + * + * This maintains WikiPage functions for backwards compatibility. + * + * @todo Move and rewrite code to an Action class + * + * See design.txt for an overview. + * Note: edit user interface and cache support functions have been + * moved to separate EditPage and HTMLFileCache classes. + * + * @internal documentation reviewed 15 Mar 2010 + */ +class Article implements Page { + /** @var IContextSource The context this Article is executed in */ + protected $mContext; + + /** @var WikiPage 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 + */ + public $mContent; + + /** + * @var Content Content of the revision we are working on + * @since 1.21 + */ + protected $mContentObject; + + /** @var bool Is the content ($mContent) already loaded? */ + protected $mContentLoaded = false; + + /** @var int|null The oldid of the article that is to be shown, 0 for the current revision */ + protected $mOldId; + + /** @var Title Title from which we were redirected here */ + protected $mRedirectedFrom = null; + + /** @var string|bool URL to redirect to or false if none */ + protected $mRedirectUrl = false; + + /** @var int Revision ID of revision we are working on */ + protected $mRevIdFetched = 0; + + /** @var Revision Revision we are working on */ + protected $mRevision = null; + + /** @var ParserOutput */ + public $mParserOutput; + + /** + * Constructor and clear the article + * @param Title $title Reference to a Title object. + * @param int $oldId Revision ID, null to fetch from request, zero for current + */ + public function __construct( Title $title, $oldId = null ) { + $this->mOldId = $oldId; + $this->mPage = $this->newPage( $title ); + } + + /** + * @param Title $title + * @return WikiPage + */ + protected function newPage( Title $title ) { + return new WikiPage( $title ); + } + + /** + * Constructor from a page id + * @param int $id Article ID to load + * @return Article|null + */ + public static function newFromID( $id ) { + $t = Title::newFromID( $id ); + # @todo FIXME: Doesn't inherit right + return $t == null ? null : new self( $t ); + # return $t == null ? null : new static( $t ); // PHP 5.3 + } + + /** + * Create an Article object of the appropriate class for the given page. + * + * @param Title $title + * @param IContextSource $context + * @return Article + */ + public static function newFromTitle( $title, IContextSource $context ) { + if ( NS_MEDIA == $title->getNamespace() ) { + // FIXME: where should this go? + $title = Title::makeTitle( NS_FILE, $title->getDBkey() ); + } + + $page = null; + wfRunHooks( 'ArticleFromTitle', array( &$title, &$page, $context ) ); + if ( !$page ) { + switch ( $title->getNamespace() ) { + case NS_FILE: + $page = new ImagePage( $title ); + break; + case NS_CATEGORY: + $page = new CategoryPage( $title ); + break; + default: + $page = new Article( $title ); + } + } + $page->setContext( $context ); + + return $page; + } + + /** + * Create an Article object of the appropriate class for the given page. + * + * @param WikiPage $page + * @param IContextSource $context + * @return Article + */ + public static function newFromWikiPage( WikiPage $page, IContextSource $context ) { + $article = self::newFromTitle( $page->getTitle(), $context ); + $article->mPage = $page; // override to keep process cached vars + return $article; + } + + /** + * Tell the page view functions that this view was redirected + * from another page on the wiki. + * @param Title $from + */ + public function setRedirectedFrom( Title $from ) { + $this->mRedirectedFrom = $from; + } + + /** + * Get the title object of the article + * + * @return Title Title object of this page + */ + public function getTitle() { + return $this->mPage->getTitle(); + } + + /** + * Get the WikiPage object of this instance + * + * @since 1.19 + * @return WikiPage + */ + public function getPage() { + return $this->mPage; + } + + /** + * Clear the object + */ + public function clear() { + $this->mContentLoaded = false; + + $this->mRedirectedFrom = null; # Title object if set + $this->mRevIdFetched = 0; + $this->mRedirectUrl = false; + + $this->mPage->clear(); + } + + /** + * Note that getContent/loadContent do not follow redirects anymore. + * If you need to fetch redirectable content easily, try + * the shortcut in WikiPage::getRedirectTarget() + * + * This function has side effects! Do not use this function if you + * only want the real revision text if any. + * + * @deprecated since 1.21; use WikiPage::getContent() instead + * + * @return string Return the text of this revision + */ + public function getContent() { + ContentHandler::deprecated( __METHOD__, '1.21' ); + $content = $this->getContentObject(); + return ContentHandler::getContentText( $content ); + } + + /** + * Returns a Content object representing the pages effective display content, + * not necessarily the revision's content! + * + * Note that getContent/loadContent do not follow redirects anymore. + * If you need to fetch redirectable content easily, try + * the shortcut in WikiPage::getRedirectTarget() + * + * 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 + * + * @since 1.21 + */ + protected function getContentObject() { + wfProfileIn( __METHOD__ ); + + 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' ); + } + } else { + $this->fetchContentObject(); + $content = $this->mContentObject; + } + + wfProfileOut( __METHOD__ ); + return $content; + } + + /** + * @return int The oldid of the article that is to be shown, 0 for the current revision + */ + public function getOldID() { + if ( is_null( $this->mOldId ) ) { + $this->mOldId = $this->getOldIDFromRequest(); + } + + return $this->mOldId; + } + + /** + * Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect + * + * @return int The old id for the request + */ + public function getOldIDFromRequest() { + $this->mRedirectUrl = false; + + $request = $this->getContext()->getRequest(); + $oldid = $request->getIntOrNull( 'oldid' ); + + if ( $oldid === null ) { + return 0; + } + + if ( $oldid !== 0 ) { + # Load the given revision and check whether the page is another one. + # In that case, update this instance to reflect the change. + if ( $oldid === $this->mPage->getLatest() ) { + $this->mRevision = $this->mPage->getRevision(); + } else { + $this->mRevision = Revision::newFromId( $oldid ); + if ( $this->mRevision !== null ) { + // Revision title doesn't match the page title given? + if ( $this->mPage->getID() != $this->mRevision->getPage() ) { + $function = array( get_class( $this->mPage ), 'newFromID' ); + $this->mPage = call_user_func( $function, $this->mRevision->getPage() ); + } + } + } + } + + if ( $request->getVal( 'direction' ) == 'next' ) { + $nextid = $this->getTitle()->getNextRevisionID( $oldid ); + if ( $nextid ) { + $oldid = $nextid; + $this->mRevision = null; + } else { + $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' ); + } + } elseif ( $request->getVal( 'direction' ) == 'prev' ) { + $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); + if ( $previd ) { + $oldid = $previd; + $this->mRevision = null; + } + } + + return $oldid; + } + + /** + * Load the revision (including text) into this object + * + * @deprecated since 1.19; use fetchContent() + */ + function loadContent() { + wfDeprecated( __METHOD__, '1.19' ); + $this->fetchContent(); + } + + /** + * Get text of an article from database + * Does *NOT* follow redirects. + * + * @protected + * @note This is really internal functionality that should really NOT be + * used by other functions. For accessing article content, use the WikiPage + * class, especially WikiBase::getContent(). However, a lot of legacy code + * uses this method to retrieve page text from the database, so the function + * has to remain public for now. + * + * @return string|bool String containing article contents, or false if null + * @deprecated since 1.21, use WikiPage::getContent() instead + */ + function fetchContent() { #BC cruft! + ContentHandler::deprecated( __METHOD__, '1.21' ); + + if ( $this->mContentLoaded && $this->mContent ) { + return $this->mContent; + } + + wfProfileIn( __METHOD__ ); + + $content = $this->fetchContentObject(); + + if ( !$content ) { + wfProfileOut( __METHOD__ ); + return false; + } + + // @todo Get rid of mContent everywhere! + $this->mContent = ContentHandler::getContentText( $content ); + ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); + + wfProfileOut( __METHOD__ ); + + return $this->mContent; + } + + /** + * Get text content object + * Does *NOT* follow redirects. + * @todo When is this null? + * + * @note Code that wants to retrieve page content from the database should + * use WikiPage::getContent(). + * + * @return Content|null|bool + * + * @since 1.21 + */ + protected function fetchContentObject() { + if ( $this->mContentLoaded ) { + return $this->mContentObject; + } + + wfProfileIn( __METHOD__ ); + + $this->mContentLoaded = true; + $this->mContent = 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', array( $oldid ), array() ); + + if ( $oldid ) { + # $this->mRevision might already be fetched by getOldIDFromRequest() + if ( !$this->mRevision ) { + $this->mRevision = Revision::newFromId( $oldid ); + if ( !$this->mRevision ) { + wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" ); + wfProfileOut( __METHOD__ ); + return false; + } + } + } else { + if ( !$this->mPage->getLatest() ) { + wfDebug( __METHOD__ . " failed to find page data for title " . + $this->getTitle()->getPrefixedText() . "\n" ); + wfProfileOut( __METHOD__ ); + return false; + } + + $this->mRevision = $this->mPage->getRevision(); + + if ( !$this->mRevision ) { + wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . + $this->mPage->getLatest() . "\n" ); + wfProfileOut( __METHOD__ ); + return false; + } + } + + // @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 + $this->mContentObject = $this->mRevision->getContent( + Revision::FOR_THIS_USER, + $this->getContext()->getUser() + ); + $this->mRevIdFetched = $this->mRevision->getId(); + + wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) ); + + wfProfileOut( __METHOD__ ); + + return $this->mContentObject; + } + + /** + * Returns true if the currently-referenced revision is the current edit + * to this page (and it exists). + * @return bool + */ + public function isCurrent() { + # If no oldid, this is the current version. + if ( $this->getOldID() == 0 ) { + return true; + } + + return $this->mPage->exists() && $this->mRevision && $this->mRevision->isCurrent(); + } + + /** + * Get the fetched Revision object depending on request parameters or null + * on failure. + * + * @since 1.19 + * @return Revision|null + */ + public function getRevisionFetched() { + $this->fetchContentObject(); + + return $this->mRevision; + } + + /** + * Use this to fetch the rev ID used on page views + * + * @return int Revision ID of last article revision + */ + public function getRevIdFetched() { + if ( $this->mRevIdFetched ) { + return $this->mRevIdFetched; + } else { + return $this->mPage->getLatest(); + } + } + + /** + * This is the default action of the index.php entry point: just view the + * page of the given title. + */ + public function view() { + global $wgUseFileCache, $wgUseETag, $wgDebugToolbar; + + wfProfileIn( __METHOD__ ); + + # Get variables from query string + # As side effect this will load the revision and update the title + # in a revision ID is passed in the request, so this should remain + # the first call of this method even if $oldid is used way below. + $oldid = $this->getOldID(); + + $user = $this->getContext()->getUser(); + # Another whitelist check in case getOldID() is altering the title + $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $user ); + if ( count( $permErrors ) ) { + wfDebug( __METHOD__ . ": denied on secondary read check\n" ); + wfProfileOut( __METHOD__ ); + throw new PermissionsError( 'read', $permErrors ); + } + + $outputPage = $this->getContext()->getOutput(); + # getOldID() may as well want us to redirect somewhere else + if ( $this->mRedirectUrl ) { + $outputPage->redirect( $this->mRedirectUrl ); + wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); + wfProfileOut( __METHOD__ ); + + return; + } + + # If we got diff in the query, we want to see a diff page instead of the article. + if ( $this->getContext()->getRequest()->getCheck( 'diff' ) ) { + wfDebug( __METHOD__ . ": showing diff page\n" ); + $this->showDiffPage(); + wfProfileOut( __METHOD__ ); + + return; + } + + # Set page title (may be overridden by DISPLAYTITLE) + $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() ); + + $outputPage->setArticleFlag( true ); + # Allow frames by default + $outputPage->allowClickjacking(); + + $parserCache = ParserCache::singleton(); + + $parserOptions = $this->getParserOptions(); + # Render printable version, use printable version cache + if ( $outputPage->isPrintable() ) { + $parserOptions->setIsPrintable( true ); + $parserOptions->setEditSection( false ); + } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) ) { + $parserOptions->setEditSection( false ); + } + + # Try client and file cache + if ( !$wgDebugToolbar && $oldid === 0 && $this->mPage->checkTouched() ) { + if ( $wgUseETag ) { + $outputPage->setETag( $parserCache->getETag( $this, $parserOptions ) ); + } + + # Is it client cached? + if ( $outputPage->checkLastModified( $this->mPage->getTouched() ) ) { + wfDebug( __METHOD__ . ": done 304\n" ); + wfProfileOut( __METHOD__ ); + + return; + # Try file cache + } elseif ( $wgUseFileCache && $this->tryFileCache() ) { + wfDebug( __METHOD__ . ": done file cache\n" ); + # tell wgOut that output is taken care of + $outputPage->disable(); + $this->mPage->doViewUpdates( $user, $oldid ); + wfProfileOut( __METHOD__ ); + + return; + } + } + + # Should the parser cache be used? + $useParserCache = $this->mPage->isParserCacheUsed( $parserOptions, $oldid ); + wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); + if ( $user->getStubThreshold() ) { + wfIncrStats( 'pcache_miss_stub' ); + } + + $this->showRedirectedFromHeader(); + $this->showNamespaceHeader(); + + # Iterate through the possible ways of constructing the output text. + # Keep going until $outputDone is set, or we run out of things to do. + $pass = 0; + $outputDone = false; + $this->mParserOutput = false; + + while ( !$outputDone && ++$pass ) { + switch ( $pass ) { + case 1: + wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$useParserCache ) ); + break; + case 2: + # Early abort if the page doesn't exist + if ( !$this->mPage->exists() ) { + wfDebug( __METHOD__ . ": showing missing article\n" ); + $this->showMissingArticle(); + $this->mPage->doViewUpdates( $user ); + wfProfileOut( __METHOD__ ); + return; + } + + # Try the parser cache + if ( $useParserCache ) { + $this->mParserOutput = $parserCache->get( $this, $parserOptions ); + + if ( $this->mParserOutput !== false ) { + if ( $oldid ) { + wfDebug( __METHOD__ . ": showing parser cache contents for current rev permalink\n" ); + $this->setOldSubtitle( $oldid ); + } else { + wfDebug( __METHOD__ . ": showing parser cache contents\n" ); + } + $outputPage->addParserOutput( $this->mParserOutput ); + # Ensure that UI elements requiring revision ID have + # the correct version information. + $outputPage->setRevisionId( $this->mPage->getLatest() ); + # Preload timestamp to avoid a DB hit + $cachedTimestamp = $this->mParserOutput->getTimestamp(); + if ( $cachedTimestamp !== null ) { + $outputPage->setRevisionTimestamp( $cachedTimestamp ); + $this->mPage->setTimestamp( $cachedTimestamp ); + } + $outputDone = true; + } + } + break; + case 3: + # This will set $this->mRevision if needed + $this->fetchContentObject(); + + # Are we looking at an old revision + if ( $oldid && $this->mRevision ) { + $this->setOldSubtitle( $oldid ); + + if ( !$this->showDeletedRevisionHeader() ) { + wfDebug( __METHOD__ . ": cannot view deleted revision\n" ); + wfProfileOut( __METHOD__ ); + return; + } + } + + # Ensure that UI elements requiring revision ID have + # the correct version information. + $outputPage->setRevisionId( $this->getRevIdFetched() ); + # Preload timestamp to avoid a DB hit + $outputPage->setRevisionTimestamp( $this->getTimestamp() ); + + # Pages containing custom CSS or JavaScript get special treatment + if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) { + wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); + $this->showCssOrJsPage(); + $outputDone = true; + } elseif ( !wfRunHooks( 'ArticleContentViewCustom', + array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) { + + # Allow extensions do their own custom view for certain pages + $outputDone = true; + } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', + array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) { + + # Allow extensions do their own custom view for certain pages + $outputDone = true; + } + break; + case 4: + # 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 ); + + if ( !$poolArticleView->execute() ) { + $error = $poolArticleView->getError(); + if ( $error ) { + $outputPage->clearHTML(); // for release() errors + $outputPage->enableClientCache( false ); + $outputPage->setRobotPolicy( 'noindex,nofollow' ); + + $errortext = $error->getWikiText( false, 'view-pool-error' ); + $outputPage->addWikiText( '<div class="errorbox">' . $errortext . '</div>' ); + } + # Connection or timeout error + wfProfileOut( __METHOD__ ); + return; + } + + $this->mParserOutput = $poolArticleView->getParserOutput(); + $outputPage->addParserOutput( $this->mParserOutput ); + if ( $content->getRedirectTarget() ) { + $outputPage->addSubtitle( wfMessage( 'redirectpagesub' )->parse() ); + } + + # Don't cache a dirty ParserOutput object + if ( $poolArticleView->getIsDirty() ) { + $outputPage->setSquidMaxage( 0 ); + $outputPage->addHTML( "<!-- parser cache is expired, " . + "sending anyway due to pool overload-->\n" ); + } + + $outputDone = true; + break; + # Should be unreachable, but just in case... + default: + break 2; + } + } + + # Get the ParserOutput actually *displayed* here. + # Note that $this->mParserOutput is the *current* version output. + $pOutput = ( $outputDone instanceof ParserOutput ) + ? $outputDone // object fetched by hook + : $this->mParserOutput; + + # Adjust title for main page & pages with displaytitle + if ( $pOutput ) { + $this->adjustDisplayTitle( $pOutput ); + } + + # For the main page, overwrite the <title> element with the con- + # tents of 'pagetitle-view-mainpage' instead of the default (if + # that's not empty). + # This message always exists because it is in the i18n files + if ( $this->getTitle()->isMainPage() ) { + $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage(); + if ( !$msg->isDisabled() ) { + $outputPage->setHTMLTitle( $msg->title( $this->getTitle() )->text() ); + } + } + + # Check for any __NOINDEX__ tags on the page using $pOutput + $policy = $this->getRobotPolicy( 'view', $pOutput ); + $outputPage->setIndexPolicy( $policy['index'] ); + $outputPage->setFollowPolicy( $policy['follow'] ); + + $this->showViewFooter(); + $this->mPage->doViewUpdates( $user, $oldid ); + + $outputPage->addModules( 'mediawiki.action.view.postEdit' ); + + wfProfileOut( __METHOD__ ); + } + + /** + * Adjust title for pages with displaytitle, -{T|}- or language conversion + * @param ParserOutput $pOutput + */ + public function adjustDisplayTitle( ParserOutput $pOutput ) { + # Adjust the title if it was set by displaytitle, -{T|}- or language conversion + $titleText = $pOutput->getTitleText(); + if ( strval( $titleText ) !== '' ) { + $this->getContext()->getOutput()->setPageTitle( $titleText ); + } + } + + /** + * Show a diff page according to current request variables. For use within + * Article::view() only, other callers should use the DifferenceEngine class. + * + * @todo Make protected + */ + public function showDiffPage() { + $request = $this->getContext()->getRequest(); + $user = $this->getContext()->getUser(); + $diff = $request->getVal( 'diff' ); + $rcid = $request->getVal( 'rcid' ); + $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) ); + $purge = $request->getVal( 'action' ) == 'purge'; + $unhide = $request->getInt( 'unhide' ) == 1; + $oldid = $this->getOldID(); + + $rev = $this->getRevisionFetched(); + + if ( !$rev ) { + $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' ) ); + $this->getContext()->getOutput()->addWikiMsg( 'difference-missing-revision', $oldid, 1 ); + return; + } + + $contentHandler = $rev->getContentHandler(); + $de = $contentHandler->createDifferenceEngine( + $this->getContext(), + $oldid, + $diff, + $rcid, + $purge, + $unhide + ); + + // DifferenceEngine directly fetched the revision: + $this->mRevIdFetched = $de->mNewid; + $de->showDiffPage( $diffOnly ); + + // Run view updates for the newer revision being diffed (and shown + // below the diff if not $diffOnly). + list( $old, $new ) = $de->mapDiffPrevNext( $oldid, $diff ); + // New can be false, convert it to 0 - this conveniently means the latest revision + $this->mPage->doViewUpdates( $user, (int)$new ); + } + + /** + * Show a page view for a page formatted as CSS or JavaScript. To be called by + * Article::view() only. + * + * This exists mostly to serve the deprecated ShowRawCssJs hook (used to customize these views). + * It has been replaced by the ContentGetParserOutput hook, which lets you do the same but with + * more flexibility. + * + * @param bool $showCacheHint Whether to show a message telling the user + * to clear the browser cache (default: true). + */ + protected function showCssOrJsPage( $showCacheHint = true ) { + $outputPage = $this->getContext()->getOutput(); + + if ( $showCacheHint ) { + $dir = $this->getContext()->getLanguage()->getDir(); + $lang = $this->getContext()->getLanguage()->getCode(); + + $outputPage->wrapWikiMsg( + "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>", + 'clearyourcache' + ); + } + + $this->fetchContentObject(); + + if ( $this->mContentObject ) { + // Give hooks a chance to customise the output + if ( ContentHandler::runLegacyHooks( + 'ShowRawCssJs', + array( $this->mContentObject, $this->getTitle(), $outputPage ) ) + ) { + // If no legacy hooks ran, display the content of the parser output, including RL modules, + // but excluding metadata like categories and language links + $po = $this->mContentObject->getParserOutput( $this->getTitle() ); + $outputPage->addParserOutputContent( $po ); + } + } + } + + /** + * Get the robot policy to be used for the current view + * @param string $action The action= GET parameter + * @param ParserOutput|null $pOutput + * @return array The policy that should be set + * @todo: actions other than 'view' + */ + public function getRobotPolicy( $action, $pOutput = null ) { + global $wgArticleRobotPolicies, $wgNamespaceRobotPolicies, $wgDefaultRobotPolicy; + + $ns = $this->getTitle()->getNamespace(); + + # Don't index user and user talk pages for blocked users (bug 11443) + if ( ( $ns == NS_USER || $ns == NS_USER_TALK ) && !$this->getTitle()->isSubpage() ) { + $specificTarget = null; + $vagueTarget = null; + $titleText = $this->getTitle()->getText(); + if ( IP::isValid( $titleText ) ) { + $vagueTarget = $titleText; + } else { + $specificTarget = $titleText; + } + if ( Block::newFromTarget( $specificTarget, $vagueTarget ) instanceof Block ) { + return array( + 'index' => 'noindex', + 'follow' => 'nofollow' + ); + } + } + + if ( $this->mPage->getID() === 0 || $this->getOldID() ) { + # Non-articles (special pages etc), and old revisions + return array( + 'index' => 'noindex', + 'follow' => 'nofollow' + ); + } elseif ( $this->getContext()->getOutput()->isPrintable() ) { + # Discourage indexing of printable versions, but encourage following + return array( + 'index' => 'noindex', + 'follow' => 'follow' + ); + } elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) { + # For ?curid=x urls, disallow indexing + return array( + 'index' => 'noindex', + 'follow' => 'follow' + ); + } + + # Otherwise, construct the policy based on the various config variables. + $policy = self::formatRobotPolicy( $wgDefaultRobotPolicy ); + + if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) { + # Honour customised robot policies for this namespace + $policy = array_merge( + $policy, + self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] ) + ); + } + if ( $this->getTitle()->canUseNoindex() && is_object( $pOutput ) && $pOutput->getIndexPolicy() ) { + # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates + # a final sanity check that we have really got the parser output. + $policy = array_merge( + $policy, + array( 'index' => $pOutput->getIndexPolicy() ) + ); + } + + if ( isset( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) ) { + # (bug 14900) site config can override user-defined __INDEX__ or __NOINDEX__ + $policy = array_merge( + $policy, + self::formatRobotPolicy( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) + ); + } + + return $policy; + } + + /** + * Converts a String robot policy into an associative array, to allow + * merging of several policies using array_merge(). + * @param array|string $policy Returns empty array on null/false/'', transparent + * to already-converted arrays, converts string. + * @return array 'index' => \<indexpolicy\>, 'follow' => \<followpolicy\> + */ + public static function formatRobotPolicy( $policy ) { + if ( is_array( $policy ) ) { + return $policy; + } elseif ( !$policy ) { + return array(); + } + + $policy = explode( ',', $policy ); + $policy = array_map( 'trim', $policy ); + + $arr = array(); + foreach ( $policy as $var ) { + if ( in_array( $var, array( 'index', 'noindex' ) ) ) { + $arr['index'] = $var; + } elseif ( in_array( $var, array( 'follow', 'nofollow' ) ) ) { + $arr['follow'] = $var; + } + } + + return $arr; + } + + /** + * If this request is a redirect view, send "redirected from" subtitle to + * the output. Returns true if the header was needed, false if this is not + * a redirect view. Handles both local and remote redirects. + * + * @return bool + */ + public function showRedirectedFromHeader() { + global $wgRedirectSources; + $outputPage = $this->getContext()->getOutput(); + + $rdfrom = $this->getContext()->getRequest()->getVal( 'rdfrom' ); + + if ( isset( $this->mRedirectedFrom ) ) { + // This is an internally redirected page view. + // We'll need a backlink to the source page for navigation. + if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { + $redir = Linker::linkKnown( + $this->mRedirectedFrom, + null, + array(), + array( 'redirect' => 'no' ) + ); + + $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); + + // Set the fragment if one was specified in the redirect + if ( $this->getTitle()->hasFragment() ) { + $outputPage->addJsConfigVars( 'wgRedirectToFragment', $this->getTitle()->getFragmentForURL() ); + $outputPage->addModules( 'mediawiki.action.view.redirectToFragment' ); + } + + // Add a <link rel="canonical"> tag + $outputPage->setCanonicalUrl( $this->getTitle()->getLocalURL() ); + + // Tell the output object that the user arrived at this article through a redirect + $outputPage->setRedirectedFrom( $this->mRedirectedFrom ); + + return true; + } + } elseif ( $rdfrom ) { + // This is an externally redirected view, from some other wiki. + // If it was reported from a trusted site, supply a backlink. + if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { + $redir = Linker::makeExternalLink( $rdfrom, $rdfrom ); + $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); + + return true; + } + } + + return false; + } + + /** + * Show a header specific to the namespace currently being viewed, like + * [[MediaWiki:Talkpagetext]]. For Article::view(). + */ + public function showNamespaceHeader() { + if ( $this->getTitle()->isTalkPage() ) { + if ( !wfMessage( 'talkpageheader' )->isDisabled() ) { + $this->getContext()->getOutput()->wrapWikiMsg( + "<div class=\"mw-talkpageheader\">\n$1\n</div>", + array( 'talkpageheader' ) + ); + } + } + } + + /** + * Show the footer section of an ordinary page view + */ + public function showViewFooter() { + # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page + if ( $this->getTitle()->getNamespace() == NS_USER_TALK + && IP::isValid( $this->getTitle()->getText() ) + ) { + $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' ); + } + + // Show a footer allowing the user to patrol the shown revision or page if possible + $patrolFooterShown = $this->showPatrolFooter(); + + wfRunHooks( 'ArticleViewFooter', array( $this, $patrolFooterShown ) ); + } + + /** + * If patrol is possible, output a patrol UI box. This is called from the + * footer section of ordinary page views. If patrol is not possible or not + * desired, does nothing. + * Side effect: When the patrol link is build, this method will call + * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax. + * + * @return bool + */ + public function showPatrolFooter() { + global $wgUseNPPatrol, $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI; + + $outputPage = $this->getContext()->getOutput(); + $user = $this->getContext()->getUser(); + $cache = wfGetMainCache(); + $rc = false; + + if ( !$this->getTitle()->quickUserCan( 'patrol', $user ) + || !( $wgUseRCPatrol || $wgUseNPPatrol ) + ) { + // Patrolling is disabled or the user isn't allowed to + return false; + } + + wfProfileIn( __METHOD__ ); + + // New page patrol: Get the timestamp of the oldest revison which + // the revision table holds for the given page. Then we look + // whether it's within the RC lifespan and if it is, we try + // to get the recentchanges row belonging to that entry + // (with rc_new = 1). + + // Check for cached results + if ( $cache->get( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ) ) ) { + wfProfileOut( __METHOD__ ); + return false; + } + + if ( $this->mRevision + && !RecentChange::isInRCLifespan( $this->mRevision->getTimestamp(), 21600 ) + ) { + // The current revision is already older than what could be in the RC table + // 6h tolerance because the RC might not be cleaned out regularly + wfProfileOut( __METHOD__ ); + return false; + } + + $dbr = wfGetDB( DB_SLAVE ); + $oldestRevisionTimestamp = $dbr->selectField( + 'revision', + 'MIN( rev_timestamp )', + array( 'rev_page' => $this->getTitle()->getArticleID() ), + __METHOD__ + ); + + if ( $oldestRevisionTimestamp + && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 ) + ) { + // 6h tolerance because the RC might not be cleaned out regularly + $rc = RecentChange::newFromConds( + array( + 'rc_new' => 1, + 'rc_timestamp' => $oldestRevisionTimestamp, + 'rc_namespace' => $this->getTitle()->getNamespace(), + 'rc_cur_id' => $this->getTitle()->getArticleID(), + 'rc_patrolled' => 0 + ), + __METHOD__, + array( 'USE INDEX' => 'new_name_timestamp' ) + ); + } + + if ( !$rc ) { + // No RC entry around + + // Cache the information we gathered above in case we can't patrol + // Don't cache in case we can patrol as this could change + $cache->set( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ), '1' ); + + wfProfileOut( __METHOD__ ); + return false; + } + + if ( $rc->getPerformer()->getName() == $user->getName() ) { + // Don't show a patrol link for own creations. If the user could + // patrol them, they already would be patrolled + wfProfileOut( __METHOD__ ); + return false; + } + + $rcid = $rc->getAttribute( 'rc_id' ); + + $token = $user->getEditToken( $rcid ); + + $outputPage->preventClickjacking(); + if ( $wgEnableAPI && $wgEnableWriteAPI && $user->isAllowed( 'writeapi' ) ) { + $outputPage->addModules( 'mediawiki.page.patrol.ajax' ); + } + + $link = Linker::linkKnown( + $this->getTitle(), + wfMessage( 'markaspatrolledtext' )->escaped(), + array(), + array( + 'action' => 'markpatrolled', + 'rcid' => $rcid, + 'token' => $token, + ) + ); + + $outputPage->addHTML( + "<div class='patrollink'>" . + wfMessage( 'markaspatrolledlink' )->rawParams( $link )->escaped() . + '</div>' + ); + + wfProfileOut( __METHOD__ ); + return true; + } + + /** + * Show the error text for a missing article. For articles in the MediaWiki + * namespace, show the default message text. To be called from Article::view(). + */ + public function showMissingArticle() { + global $wgSend404Code; + $outputPage = $this->getContext()->getOutput(); + // Whether the page is a root user page of an existing user (but not a subpage) + $validUserPage = false; + + # Show info in user (talk) namespace. Does the user exist? Is he blocked? + if ( $this->getTitle()->getNamespace() == NS_USER + || $this->getTitle()->getNamespace() == NS_USER_TALK + ) { + $parts = explode( '/', $this->getTitle()->getText() ); + $rootPart = $parts[0]; + $user = User::newFromName( $rootPart, false /* allow IP users*/ ); + $ip = User::isIP( $rootPart ); + + if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist + $outputPage->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", + array( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ) ); + } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked + LogEventsList::showLogExtract( + $outputPage, + 'block', + $user->getUserPage(), + '', + array( + 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => array( + 'blocked-notice-logextract', + $user->getName() # Support GENDER in notice + ) + ) + ); + $validUserPage = !$this->getTitle()->isSubpage(); + } else { + $validUserPage = !$this->getTitle()->isSubpage(); + } + } + + wfRunHooks( 'ShowMissingArticle', array( $this ) ); + + // Give extensions a chance to hide their (unrelated) log entries + $logTypes = array( 'delete', 'move' ); + $conds = array( "log_action != 'revision'" ); + wfRunHooks( 'Article::MissingArticleConditions', array( &$conds, $logTypes ) ); + + # Show delete and move logs + LogEventsList::showLogExtract( $outputPage, $logTypes, $this->getTitle(), '', + array( 'lim' => 10, + 'conds' => $conds, + 'showIfEmpty' => false, + 'msgKey' => array( 'moveddeleted-notice' ) ) + ); + + if ( !$this->mPage->hasViewableContent() && $wgSend404Code && !$validUserPage ) { + // If there's no backing content, send a 404 Not Found + // for better machine handling of broken links. + $this->getContext()->getRequest()->response()->header( "HTTP/1.1 404 Not Found" ); + } + + if ( $validUserPage ) { + // Also apply the robot policy for nonexisting user pages (as those aren't served as 404) + $policy = $this->getRobotPolicy( 'view' ); + $outputPage->setIndexPolicy( $policy['index'] ); + $outputPage->setFollowPolicy( $policy['follow'] ); + } + + $hookResult = wfRunHooks( 'BeforeDisplayNoArticleText', array( $this ) ); + + if ( ! $hookResult ) { + return; + } + + # Show error message + $oldid = $this->getOldID(); + if ( $oldid ) { + $text = wfMessage( 'missing-revision', $oldid )->plain(); + } elseif ( $this->getTitle()->getNamespace() === NS_MEDIAWIKI ) { + // Use the default message text + $text = $this->getTitle()->getDefaultMessageText(); + } elseif ( $this->getTitle()->quickUserCan( 'create', $this->getContext()->getUser() ) + && $this->getTitle()->quickUserCan( 'edit', $this->getContext()->getUser() ) + ) { + $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; + $text = wfMessage( $message )->plain(); + } else { + $text = wfMessage( 'noarticletext-nopermission' )->plain(); + } + $text = "<div class='noarticletext'>\n$text\n</div>"; + + $outputPage->addWikiText( $text ); + } + + /** + * If the revision requested for view is deleted, check permissions. + * Send either an error message or a warning header to the output. + * + * @return bool true if the view is allowed, false if not. + */ + public function showDeletedRevisionHeader() { + if ( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + // Not deleted + return true; + } + + $outputPage = $this->getContext()->getOutput(); + $user = $this->getContext()->getUser(); + // If the user is not allowed to see it... + if ( !$this->mRevision->userCan( Revision::DELETED_TEXT, $user ) ) { + $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", + 'rev-deleted-text-permission' ); + + return false; + // If the user needs to confirm that they want to see it... + } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) { + # Give explanation and add a link to view the revision... + $oldid = intval( $this->getOldID() ); + $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" ); + $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? + 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide'; + $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", + array( $msg, $link ) ); + + return false; + // We are allowed to see... + } else { + $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? + 'rev-suppressed-text-view' : 'rev-deleted-text-view'; + $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg ); + + return true; + } + } + + /** + * Generate the navigation links when browsing through an article revisions + * It shows the information as: + * Revision as of \<date\>; view current revision + * \<- Previous version | Next Version -\> + * + * @param int $oldid Revision ID of this article revision + */ + public function setOldSubtitle( $oldid = 0 ) { + if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { + return; + } + + $unhide = $this->getContext()->getRequest()->getInt( 'unhide' ) == 1; + + # Cascade unhide param in links for easy deletion browsing + $extraParams = array(); + if ( $unhide ) { + $extraParams['unhide'] = 1; + } + + if ( $this->mRevision && $this->mRevision->getId() === $oldid ) { + $revision = $this->mRevision; + } else { + $revision = Revision::newFromId( $oldid ); + } + + $timestamp = $revision->getTimestamp(); + + $current = ( $oldid == $this->mPage->getLatest() ); + $language = $this->getContext()->getLanguage(); + $user = $this->getContext()->getUser(); + + $td = $language->userTimeAndDate( $timestamp, $user ); + $tddate = $language->userDate( $timestamp, $user ); + $tdtime = $language->userTime( $timestamp, $user ); + + # Show user links if allowed to see them. If hidden, then show them only if requested... + $userlinks = Linker::revUserTools( $revision, !$unhide ); + + $infomsg = $current && !wfMessage( 'revision-info-current' )->isDisabled() + ? 'revision-info-current' + : 'revision-info'; + + $outputPage = $this->getContext()->getOutput(); + $outputPage->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg, + $td )->rawParams( $userlinks )->params( $revision->getID(), $tddate, + $tdtime, $revision->getUser() )->rawParams( Linker::revComment( $revision, true, true ) )->parse() . "</div>" ); + + $lnk = $current + ? wfMessage( 'currentrevisionlink' )->escaped() + : Linker::linkKnown( + $this->getTitle(), + wfMessage( 'currentrevisionlink' )->escaped(), + array(), + $extraParams + ); + $curdiff = $current + ? wfMessage( 'diff' )->escaped() + : Linker::linkKnown( + $this->getTitle(), + wfMessage( 'diff' )->escaped(), + array(), + array( + 'diff' => 'cur', + 'oldid' => $oldid + ) + $extraParams + ); + $prev = $this->getTitle()->getPreviousRevisionID( $oldid ); + $prevlink = $prev + ? Linker::linkKnown( + $this->getTitle(), + wfMessage( 'previousrevision' )->escaped(), + array(), + array( + 'direction' => 'prev', + 'oldid' => $oldid + ) + $extraParams + ) + : wfMessage( 'previousrevision' )->escaped(); + $prevdiff = $prev + ? Linker::linkKnown( + $this->getTitle(), + wfMessage( 'diff' )->escaped(), + array(), + array( + 'diff' => 'prev', + 'oldid' => $oldid + ) + $extraParams + ) + : wfMessage( 'diff' )->escaped(); + $nextlink = $current + ? wfMessage( 'nextrevision' )->escaped() + : Linker::linkKnown( + $this->getTitle(), + wfMessage( 'nextrevision' )->escaped(), + array(), + array( + 'direction' => 'next', + 'oldid' => $oldid + ) + $extraParams + ); + $nextdiff = $current + ? wfMessage( 'diff' )->escaped() + : Linker::linkKnown( + $this->getTitle(), + wfMessage( 'diff' )->escaped(), + array(), + array( + 'diff' => 'next', + 'oldid' => $oldid + ) + $extraParams + ); + + $cdel = Linker::getRevDeleteLink( $user, $revision, $this->getTitle() ); + if ( $cdel !== '' ) { + $cdel .= ' '; + } + + $outputPage->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel . + wfMessage( 'revision-nav' )->rawParams( + $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff + )->escaped() . "</div>" ); + } + + /** + * Return the HTML for the top of a redirect page + * + * Chances are you should just be using the ParserOutput from + * WikitextContent::getParserOutput instead of calling this for redirects. + * + * @param Title|array $target Destination(s) to redirect + * @param bool $appendSubtitle [optional] + * @param bool $forceKnown Should the image be shown as a bluelink regardless of existence? + * @return string Containing HMTL with redirect link + */ + public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { + $lang = $this->getTitle()->getPageLanguage(); + if ( $appendSubtitle ) { + $out = $this->getContext()->getOutput(); + $out->addSubtitle( wfMessage( 'redirectpagesub' )->parse() ); + } + return static::getRedirectHeaderHtml( $lang, $target, $forceKnown ); + } + + /** + * Return the HTML for the top of a redirect page + * + * Chances are you should just be using the ParserOutput from + * WikitextContent::getParserOutput instead of calling this for redirects. + * + * @since 1.23 + * @param Language $lang + * @param Title|array $target Destination(s) to redirect + * @param bool $forceKnown Should the image be shown as a bluelink regardless of existence? + * @return string Containing HMTL with redirect link + */ + public static function getRedirectHeaderHtml( Language $lang, $target, $forceKnown = false ) { + global $wgStylePath; + + if ( !is_array( $target ) ) { + $target = array( $target ); + } + + $imageDir = $lang->getDir(); + + // the loop prepends the arrow image before the link, so the first case needs to be outside + + /** @var $title Title */ + $title = array_shift( $target ); + + if ( $forceKnown ) { + $link = Linker::linkKnown( $title, htmlspecialchars( $title->getFullText() ) ); + } else { + $link = Linker::link( $title, htmlspecialchars( $title->getFullText() ) ); + } + + $nextRedirect = $wgStylePath . '/common/images/nextredirect' . $imageDir . '.png'; + $alt = $lang->isRTL() ? '←' : '→'; + + // Automatically append redirect=no to each link, since most of them are + // redirect pages themselves. + /** @var Title $rt */ + foreach ( $target as $rt ) { + $link .= Html::element( 'img', array( 'src' => $nextRedirect, 'alt' => $alt ) ); + if ( $forceKnown ) { + $link .= Linker::linkKnown( + $rt, + htmlspecialchars( $rt->getFullText(), + array(), + array( 'redirect' => 'no' ) + ) + ); + } else { + $link .= Linker::link( + $rt, + htmlspecialchars( $rt->getFullText() ), + array(), + array( 'redirect' => 'no' ) + ); + } + } + + $imageUrl = $wgStylePath . '/common/images/redirect' . $imageDir . '.png'; + return '<div class="redirectMsg">' . + Html::element( 'img', array( 'src' => $imageUrl, 'alt' => '#REDIRECT' ) ) . + '<span class="redirectText">' . $link . '</span></div>'; + } + + /** + * Handle action=render + */ + public function render() { + $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' ); + $this->getContext()->getOutput()->setArticleBodyOnly( true ); + $this->getContext()->getOutput()->enableSectionEditLinks( false ); + $this->view(); + } + + /** + * action=protect handler + */ + public function protect() { + $form = new ProtectionForm( $this ); + $form->execute(); + } + + /** + * action=unprotect handler (alias) + */ + public function unprotect() { + $this->protect(); + } + + /** + * UI entry point for page deletion + */ + public function delete() { + # This code desperately needs to be totally rewritten + + $title = $this->getTitle(); + $user = $this->getContext()->getUser(); + + # Check permissions + $permission_errors = $title->getUserPermissionsErrors( 'delete', $user ); + if ( count( $permission_errors ) ) { + throw new PermissionsError( 'delete', $permission_errors ); + } + + # Read-only check... + if ( wfReadOnly() ) { + throw new ReadOnlyError; + } + + # Better double-check that it hasn't been deleted yet! + $this->mPage->loadPageData( 'fromdbmaster' ); + if ( !$this->mPage->exists() ) { + $deleteLogPage = new LogPage( 'delete' ); + $outputPage = $this->getContext()->getOutput(); + $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) ); + $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>", + array( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) ) + ); + $outputPage->addHTML( + Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) + ); + LogEventsList::showLogExtract( + $outputPage, + 'delete', + $title + ); + + return; + } + + $request = $this->getContext()->getRequest(); + $deleteReasonList = $request->getText( 'wpDeleteReasonList', 'other' ); + $deleteReason = $request->getText( 'wpReason' ); + + if ( $deleteReasonList == 'other' ) { + $reason = $deleteReason; + } elseif ( $deleteReason != '' ) { + // Entry from drop down menu + additional comment + $colonseparator = wfMessage( 'colon-separator' )->inContentLanguage()->text(); + $reason = $deleteReasonList . $colonseparator . $deleteReason; + } else { + $reason = $deleteReasonList; + } + + if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ), + array( 'delete', $this->getTitle()->getPrefixedText() ) ) + ) { + # Flag to hide all contents of the archived revisions + $suppress = $request->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' ); + + $this->doDelete( $reason, $suppress ); + + WatchAction::doWatchOrUnwatch( $request->getCheck( 'wpWatch' ), $title, $user ); + + return; + } + + // Generate deletion reason + $hasHistory = false; + if ( !$reason ) { + try { + $reason = $this->generateReason( $hasHistory ); + } catch ( MWException $e ) { + # if a page is horribly broken, we still want to be able to + # delete it. So be lenient about errors here. + wfDebug( "Error while building auto delete summary: $e" ); + $reason = ''; + } + } + + // If the page has a history, insert a warning + if ( $hasHistory ) { + $revisions = $this->mTitle->estimateRevisionCount(); + // @todo FIXME: i18n issue/patchwork message + $this->getContext()->getOutput()->addHTML( '<strong class="mw-delete-warning-revisions">' . + wfMessage( 'historywarning' )->numParams( $revisions )->parse() . + wfMessage( 'word-separator' )->plain() . Linker::linkKnown( $title, + wfMessage( 'history' )->escaped(), + array( 'rel' => 'archives' ), + array( 'action' => 'history' ) ) . + '</strong>' + ); + + if ( $this->mTitle->isBigDeletion() ) { + global $wgDeleteRevisionsLimit; + $this->getContext()->getOutput()->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", + array( + 'delete-warning-toobig', + $this->getContext()->getLanguage()->formatNum( $wgDeleteRevisionsLimit ) + ) + ); + } + } + + $this->confirmDelete( $reason ); + } + + /** + * Output deletion confirmation dialog + * @todo FIXME: Move to another file? + * @param string $reason Prefilled reason + */ + public function confirmDelete( $reason ) { + wfDebug( "Article::confirmDelete\n" ); + + $outputPage = $this->getContext()->getOutput(); + $outputPage->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) ); + $outputPage->addBacklinkSubtitle( $this->getTitle() ); + $outputPage->setRobotPolicy( 'noindex,nofollow' ); + $backlinkCache = $this->getTitle()->getBacklinkCache(); + if ( $backlinkCache->hasLinks( 'pagelinks' ) || $backlinkCache->hasLinks( 'templatelinks' ) ) { + $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", + 'deleting-backlinks-warning' ); + } + $outputPage->addWikiMsg( 'confirmdeletetext' ); + + wfRunHooks( 'ArticleConfirmDelete', array( $this, $outputPage, &$reason ) ); + + $user = $this->getContext()->getUser(); + + if ( $user->isAllowed( 'suppressrevision' ) ) { + $suppress = "<tr id=\"wpDeleteSuppressRow\"> + <td></td> + <td class='mw-input'><strong>" . + Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(), + 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) . + "</strong></td> + </tr>"; + } else { + $suppress = ''; + } + $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $user->isWatched( $this->getTitle() ); + + $form = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $this->getTitle()->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . + Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) . + Xml::tags( 'legend', null, wfMessage( 'delete-legend' )->escaped() ) . + Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) . + "<tr id=\"wpDeleteReasonListRow\"> + <td class='mw-label'>" . + Xml::label( wfMessage( 'deletecomment' )->text(), 'wpDeleteReasonList' ) . + "</td> + <td class='mw-input'>" . + Xml::listDropDown( + 'wpDeleteReasonList', + wfMessage( 'deletereason-dropdown' )->inContentLanguage()->text(), + wfMessage( 'deletereasonotherlist' )->inContentLanguage()->text(), + '', + 'wpReasonDropDown', + 1 + ) . + "</td> + </tr> + <tr id=\"wpDeleteReasonRow\"> + <td class='mw-label'>" . + Xml::label( wfMessage( 'deleteotherreason' )->text(), 'wpReason' ) . + "</td> + <td class='mw-input'>" . + Html::input( 'wpReason', $reason, 'text', array( + 'size' => '60', + 'maxlength' => '255', + 'tabindex' => '2', + 'id' => 'wpReason', + 'autofocus' + ) ) . + "</td> + </tr>"; + + # Disallow watching if user is not logged in + if ( $user->isLoggedIn() ) { + $form .= " + <tr> + <td></td> + <td class='mw-input'>" . + Xml::checkLabel( wfMessage( 'watchthis' )->text(), + 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) . + "</td> + </tr>"; + } + + $form .= " + $suppress + <tr> + <td></td> + <td class='mw-submit'>" . + Xml::submitButton( wfMessage( 'deletepage' )->text(), + array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) . + "</td> + </tr>" . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) . + Html::hidden( + 'wpEditToken', + $user->getEditToken( array( 'delete', $this->getTitle()->getPrefixedText() ) ) + ) . + Xml::closeElement( 'form' ); + + if ( $user->isAllowed( 'editinterface' ) ) { + $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ); + $link = Linker::link( + $title, + wfMessage( 'delete-edit-reasonlist' )->escaped(), + array(), + array( 'action' => 'edit' ) + ); + $form .= '<p class="mw-delete-editreasons">' . $link . '</p>'; + } + + $outputPage->addHTML( $form ); + + $deleteLogPage = new LogPage( 'delete' ); + $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) ); + LogEventsList::showLogExtract( $outputPage, 'delete', + $this->getTitle() + ); + } + + /** + * Perform a deletion and output success or failure messages + * @param string $reason + * @param bool $suppress + */ + public function doDelete( $reason, $suppress = false ) { + $error = ''; + $outputPage = $this->getContext()->getOutput(); + $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error ); + + if ( $status->isGood() ) { + $deleted = $this->getTitle()->getPrefixedText(); + + $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) ); + $outputPage->setRobotPolicy( 'noindex,nofollow' ); + + $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]'; + + $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink ); + $outputPage->returnToMain( false ); + } else { + $outputPage->setPageTitle( + wfMessage( 'cannotdelete-title', + $this->getTitle()->getPrefixedText() ) + ); + + if ( $error == '' ) { + $outputPage->addWikiText( + "<div class=\"error mw-error-cannotdelete\">\n" . $status->getWikiText() . "\n</div>" + ); + $deleteLogPage = new LogPage( 'delete' ); + $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) ); + + LogEventsList::showLogExtract( + $outputPage, + 'delete', + $this->getTitle() + ); + } else { + $outputPage->addHTML( $error ); + } + } + } + + /* Caching functions */ + + /** + * checkLastModified returns true if it has taken care of all + * output to the client that is necessary for this request. + * (that is, it has sent a cached version of the page) + * + * @return bool true if cached version send, false otherwise + */ + protected function tryFileCache() { + static $called = false; + + if ( $called ) { + wfDebug( "Article::tryFileCache(): called twice!?\n" ); + return false; + } + + $called = true; + if ( $this->isFileCacheable() ) { + $cache = HTMLFileCache::newFromTitle( $this->getTitle(), 'view' ); + if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) { + wfDebug( "Article::tryFileCache(): about to load file\n" ); + $cache->loadFromFileCache( $this->getContext() ); + return true; + } else { + wfDebug( "Article::tryFileCache(): starting buffer\n" ); + ob_start( array( &$cache, 'saveToFileCache' ) ); + } + } else { + wfDebug( "Article::tryFileCache(): not cacheable\n" ); + } + + return false; + } + + /** + * Check if the page can be cached + * @return bool + */ + public function isFileCacheable() { + $cacheable = false; + + if ( HTMLFileCache::useFileCache( $this->getContext() ) ) { + $cacheable = $this->mPage->getID() + && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect(); + // Extension may have reason to disable file caching on some pages. + if ( $cacheable ) { + $cacheable = wfRunHooks( 'IsFileCacheable', array( &$this ) ); + } + } + + return $cacheable; + } + + /**#@-*/ + + /** + * Lightweight method to get the parser output for a page, checking the parser cache + * and so on. Doesn't consider most of the stuff that WikiPage::view is forced to + * consider, so it's not appropriate to use there. + * + * @since 1.16 (r52326) for LiquidThreads + * + * @param int|null $oldid Revision ID or null + * @param User $user The relevant user + * @return ParserOutput|bool ParserOutput or false if the given revision ID is not found + */ + public function getParserOutput( $oldid = null, User $user = null ) { + //XXX: bypasses mParserOptions and thus setParserOptions() + + if ( $user === null ) { + $parserOptions = $this->getParserOptions(); + } else { + $parserOptions = $this->mPage->makeParserOptions( $user ); + } + + return $this->mPage->getParserOutput( $parserOptions, $oldid ); + } + + /** + * Override the ParserOptions used to render the primary article wikitext. + * + * @param ParserOptions $options + * @throws MWException if the parser options where already initialized. + */ + public function setParserOptions( ParserOptions $options ) { + if ( $this->mParserOptions ) { + throw new MWException( "can't change parser options after they have already been set" ); + } + + // clone, so if $options is modified later, it doesn't confuse the parser cache. + $this->mParserOptions = clone $options; + } + + /** + * Get parser options suitable for rendering the primary article wikitext + * @return ParserOptions + */ + public function getParserOptions() { + if ( !$this->mParserOptions ) { + $this->mParserOptions = $this->mPage->makeParserOptions( $this->getContext() ); + } + // Clone to allow modifications of the return value without affecting cache + return clone $this->mParserOptions; + } + + /** + * Sets the context this Article is executed in + * + * @param IContextSource $context + * @since 1.18 + */ + public function setContext( $context ) { + $this->mContext = $context; + } + + /** + * Gets the context this Article is executed in + * + * @return IContextSource + * @since 1.18 + */ + public function getContext() { + if ( $this->mContext instanceof IContextSource ) { + return $this->mContext; + } else { + wfDebug( __METHOD__ . " called and \$mContext is null. " . + "Return RequestContext::getMain(); for sanity\n" ); + return RequestContext::getMain(); + } + } + + /** + * Use PHP's magic __get handler to handle accessing of + * raw WikiPage fields for backwards compatibility. + * + * @param string $fname Field name + */ + public function __get( $fname ) { + if ( property_exists( $this->mPage, $fname ) ) { + #wfWarn( "Access to raw $fname field " . __CLASS__ ); + return $this->mPage->$fname; + } + trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE ); + } + + /** + * Use PHP's magic __set handler to handle setting of + * raw WikiPage fields for backwards compatibility. + * + * @param string $fname Field name + * @param mixed $fvalue New value + */ + public function __set( $fname, $fvalue ) { + if ( property_exists( $this->mPage, $fname ) ) { + #wfWarn( "Access to raw $fname field of " . __CLASS__ ); + $this->mPage->$fname = $fvalue; + // Note: extensions may want to toss on new fields + } elseif ( !in_array( $fname, array( 'mContext', 'mPage' ) ) ) { + $this->mPage->$fname = $fvalue; + } else { + trigger_error( 'Inaccessible property via __set(): ' . $fname, E_USER_NOTICE ); + } + } + + /** + * Use PHP's magic __call handler to transform instance calls to + * WikiPage functions for backwards compatibility. + * + * @param string $fname Name of called method + * @param array $args Arguments to the method + * @return mixed + */ + public function __call( $fname, $args ) { + if ( is_callable( array( $this->mPage, $fname ) ) ) { + #wfWarn( "Call to " . __CLASS__ . "::$fname; please use WikiPage instead" ); + return call_user_func_array( array( $this->mPage, $fname ), $args ); + } + trigger_error( 'Inaccessible function via __call(): ' . $fname, E_USER_ERROR ); + } + + // ****** B/C functions to work-around PHP silliness with __call and references ****** // + + /** + * @param array $limit + * @param array $expiry + * @param bool $cascade + * @param string $reason + * @param User $user + * @return Status + */ + public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, + $reason, User $user + ) { + return $this->mPage->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user ); + } + + /** + * @param array $limit + * @param string $reason + * @param int $cascade + * @param array $expiry + * @return bool + */ + public function updateRestrictions( $limit = array(), $reason = '', + &$cascade = 0, $expiry = array() + ) { + return $this->mPage->doUpdateRestrictions( + $limit, + $expiry, + $cascade, + $reason, + $this->getContext()->getUser() + ); + } + + /** + * @param string $reason + * @param bool $suppress + * @param int $id + * @param bool $commit + * @param string $error + * @return bool + */ + public function doDeleteArticle( $reason, $suppress = false, $id = 0, + $commit = true, &$error = '' + ) { + return $this->mPage->doDeleteArticle( $reason, $suppress, $id, $commit, $error ); + } + + /** + * @param string $fromP + * @param string $summary + * @param string $token + * @param bool $bot + * @param array $resultDetails + * @param User|null $user + * @return array + */ + public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user = null ) { + $user = is_null( $user ) ? $this->getContext()->getUser() : $user; + return $this->mPage->doRollback( $fromP, $summary, $token, $bot, $resultDetails, $user ); + } + + /** + * @param string $fromP + * @param string $summary + * @param bool $bot + * @param array $resultDetails + * @param User|null $guser + * @return array + */ + public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser = null ) { + $guser = is_null( $guser ) ? $this->getContext()->getUser() : $guser; + return $this->mPage->commitRollback( $fromP, $summary, $bot, $resultDetails, $guser ); + } + + /** + * @param bool $hasHistory + * @return mixed + */ + public function generateReason( &$hasHistory ) { + $title = $this->mPage->getTitle(); + $handler = ContentHandler::getForTitle( $title ); + return $handler->getAutoDeleteReason( $title, $hasHistory ); + } + + // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** // + + /** + * @return array + */ + public static function selectFields() { + return WikiPage::selectFields(); + } + + /** + * @param Title $title + */ + public static function onArticleCreate( $title ) { + WikiPage::onArticleCreate( $title ); + } + + /** + * @param Title $title + */ + public static function onArticleDelete( $title ) { + WikiPage::onArticleDelete( $title ); + } + + /** + * @param Title $title + */ + public static function onArticleEdit( $title ) { + WikiPage::onArticleEdit( $title ); + } + + /** + * @param string $oldtext + * @param string $newtext + * @param int $flags + * @return string + * @deprecated since 1.21, use ContentHandler::getAutosummary() instead + */ + public static function getAutosummary( $oldtext, $newtext, $flags ) { + return WikiPage::getAutosummary( $oldtext, $newtext, $flags ); + } + // ****** +} diff --git a/includes/page/CategoryPage.php b/includes/page/CategoryPage.php new file mode 100644 index 0000000000..9abc6a89b0 --- /dev/null +++ b/includes/page/CategoryPage.php @@ -0,0 +1,118 @@ +<?php +/** + * Special handling for category description pages. + * Modelled after ImagePage.php. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Special handling for category description pages, showing pages, + * subcategories and file that belong to the category + */ +class CategoryPage extends Article { + # Subclasses can change this to override the viewer class. + protected $mCategoryViewerClass = 'CategoryViewer'; + + /** + * @param Title $title + * @return WikiCategoryPage + */ + protected function newPage( Title $title ) { + // Overload mPage with a category-specific page + return new WikiCategoryPage( $title ); + } + + /** + * Constructor from a page id + * @param int $id Article ID to load + * @return CategoryPage|null + */ + public static function newFromID( $id ) { + $t = Title::newFromID( $id ); + # @todo FIXME: Doesn't inherit right + return $t == null ? null : new self( $t ); + # return $t == null ? null : new static( $t ); // PHP 5.3 + } + + function view() { + $request = $this->getContext()->getRequest(); + $diff = $request->getVal( 'diff' ); + $diffOnly = $request->getBool( 'diffonly', + $this->getContext()->getUser()->getOption( 'diffonly' ) ); + + if ( $diff !== null && $diffOnly ) { + parent::view(); + return; + } + + if ( !wfRunHooks( 'CategoryPageView', array( &$this ) ) ) { + return; + } + + $title = $this->getTitle(); + if ( NS_CATEGORY == $title->getNamespace() ) { + $this->openShowCategory(); + } + + parent::view(); + + if ( NS_CATEGORY == $title->getNamespace() ) { + $this->closeShowCategory(); + } + } + + function openShowCategory() { + # For overloading + } + + function closeShowCategory() { + // Use these as defaults for back compat --catrope + $request = $this->getContext()->getRequest(); + $oldFrom = $request->getVal( 'from' ); + $oldUntil = $request->getVal( 'until' ); + + $reqArray = $request->getValues(); + + $from = $until = array(); + foreach ( array( 'page', 'subcat', 'file' ) as $type ) { + $from[$type] = $request->getVal( "{$type}from", $oldFrom ); + $until[$type] = $request->getVal( "{$type}until", $oldUntil ); + + // Do not want old-style from/until propagating in nav links. + if ( !isset( $reqArray["{$type}from"] ) && isset( $reqArray["from"] ) ) { + $reqArray["{$type}from"] = $reqArray["from"]; + } + if ( !isset( $reqArray["{$type}to"] ) && isset( $reqArray["to"] ) ) { + $reqArray["{$type}to"] = $reqArray["to"]; + } + } + + unset( $reqArray["from"] ); + unset( $reqArray["to"] ); + + $viewer = new $this->mCategoryViewerClass( + $this->getContext()->getTitle(), + $this->getContext(), + $from, + $until, + $reqArray + ); + $this->getContext()->getOutput()->addHTML( $viewer->getHTML() ); + } +} diff --git a/includes/page/ImagePage.php b/includes/page/ImagePage.php new file mode 100644 index 0000000000..60db2025c2 --- /dev/null +++ b/includes/page/ImagePage.php @@ -0,0 +1,1567 @@ +<?php +/** + * Special handling for file description pages. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Class for viewing MediaWiki file description pages + * + * @ingroup Media + */ +class ImagePage extends Article { + /** @var File */ + private $displayImg; + + /** @var FileRepo */ + private $repo; + + /** @var bool */ + private $fileLoaded; + + /** @var bool */ + protected $mExtraDescription = false; + + /** + * @param Title $title + * @return WikiFilePage + */ + protected function newPage( Title $title ) { + // Overload mPage with a file-specific page + return new WikiFilePage( $title ); + } + + /** + * Constructor from a page id + * @param int $id Article ID to load + * @return ImagePage|null + */ + public static function newFromID( $id ) { + $t = Title::newFromID( $id ); + # @todo FIXME: Doesn't inherit right + return $t == null ? null : new self( $t ); + # return $t == null ? null : new static( $t ); // PHP 5.3 + } + + /** + * @param File $file + * @return void + */ + public function setFile( $file ) { + $this->mPage->setFile( $file ); + $this->displayImg = $file; + $this->fileLoaded = true; + } + + protected function loadFile() { + if ( $this->fileLoaded ) { + return; + } + $this->fileLoaded = true; + + $this->displayImg = $img = false; + wfRunHooks( 'ImagePageFindFile', array( $this, &$img, &$this->displayImg ) ); + if ( !$img ) { // not set by hook? + $img = wfFindFile( $this->getTitle() ); + if ( !$img ) { + $img = wfLocalFile( $this->getTitle() ); + } + } + $this->mPage->setFile( $img ); + if ( !$this->displayImg ) { // not set by hook? + $this->displayImg = $img; + } + $this->repo = $img->getRepo(); + } + + /** + * Handler for action=render + * Include body text only; none of the image extras + */ + public function render() { + $this->getContext()->getOutput()->setArticleBodyOnly( true ); + parent::view(); + } + + public function view() { + global $wgShowEXIF; + + $out = $this->getContext()->getOutput(); + $request = $this->getContext()->getRequest(); + $diff = $request->getVal( 'diff' ); + $diffOnly = $request->getBool( + 'diffonly', + $this->getContext()->getUser()->getOption( 'diffonly' ) + ); + + if ( $this->getTitle()->getNamespace() != NS_FILE || ( $diff !== null && $diffOnly ) ) { + parent::view(); + return; + } + + $this->loadFile(); + + if ( $this->getTitle()->getNamespace() == NS_FILE && $this->mPage->getFile()->getRedirected() ) { + if ( $this->getTitle()->getDBkey() == $this->mPage->getFile()->getName() || $diff !== null ) { + // mTitle is the same as the redirect target so ask Article + // to perform the redirect for us. + $request->setVal( 'diffonly', 'true' ); + parent::view(); + return; + } else { + // mTitle is not the same as the redirect target so it is + // probably the redirect page itself. Fake the redirect symbol + $out->setPageTitle( $this->getTitle()->getPrefixedText() ); + $out->addHTML( $this->viewRedirect( + Title::makeTitle( NS_FILE, $this->mPage->getFile()->getName() ), + /* $appendSubtitle */ true, + /* $forceKnown */ true ) + ); + $this->mPage->doViewUpdates( $this->getContext()->getUser(), $this->getOldID() ); + return; + } + } + + if ( $wgShowEXIF && $this->displayImg->exists() ) { + // @todo FIXME: Bad interface, see note on MediaHandler::formatMetadata(). + $formattedMetadata = $this->displayImg->formatMetadata(); + $showmeta = $formattedMetadata !== false; + } else { + $showmeta = false; + } + + if ( !$diff && $this->displayImg->exists() ) { + $out->addHTML( $this->showTOC( $showmeta ) ); + } + + if ( !$diff ) { + $this->openShowImage(); + } + + # No need to display noarticletext, we use our own message, output in openShowImage() + if ( $this->mPage->getID() ) { + # NS_FILE is in the user language, but this section (the actual wikitext) + # should be in page content language + $pageLang = $this->getTitle()->getPageViewLanguage(); + $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content', + 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(), + 'class' => 'mw-content-' . $pageLang->getDir() ) ) ); + + parent::view(); + + $out->addHTML( Xml::closeElement( 'div' ) ); + } else { + # Just need to set the right headers + $out->setArticleFlag( true ); + $out->setPageTitle( $this->getTitle()->getPrefixedText() ); + $this->mPage->doViewUpdates( $this->getContext()->getUser(), $this->getOldID() ); + } + + # Show shared description, if needed + if ( $this->mExtraDescription ) { + $fol = wfMessage( 'shareddescriptionfollows' ); + if ( !$fol->isDisabled() ) { + $out->addWikiText( $fol->plain() ); + } + $out->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" ); + } + + $this->closeShowImage(); + $this->imageHistory(); + // TODO: Cleanup the following + + $out->addHTML( Xml::element( 'h2', + array( 'id' => 'filelinks' ), + wfMessage( 'imagelinks' )->text() ) . "\n" ); + $this->imageDupes(); + # @todo FIXME: For some freaky reason, we can't redirect to foreign images. + # Yet we return metadata about the target. Definitely an issue in the FileRepo + $this->imageLinks(); + + # Allow extensions to add something after the image links + $html = ''; + wfRunHooks( 'ImagePageAfterImageLinks', array( $this, &$html ) ); + if ( $html ) { + $out->addHTML( $html ); + } + + if ( $showmeta ) { + $out->addHTML( Xml::element( + 'h2', + array( 'id' => 'metadata' ), + wfMessage( 'metadata' )->text() ) . "\n" ); + $out->addWikiText( $this->makeMetadataTable( $formattedMetadata ) ); + $out->addModules( array( 'mediawiki.action.view.metadata' ) ); + } + + // Add remote Filepage.css + if ( !$this->repo->isLocal() ) { + $css = $this->repo->getDescriptionStylesheetUrl(); + if ( $css ) { + $out->addStyle( $css ); + } + } + // always show the local local Filepage.css, bug 29277 + $out->addModuleStyles( 'filepage' ); + } + + /** + * @return File + */ + public function getDisplayedFile() { + $this->loadFile(); + return $this->displayImg; + } + + /** + * Create the TOC + * + * @param bool $metadata Whether or not to show the metadata link + * @return string + */ + protected function showTOC( $metadata ) { + $r = array( + '<li><a href="#file">' . wfMessage( 'file-anchor-link' )->escaped() . '</a></li>', + '<li><a href="#filehistory">' . wfMessage( 'filehist' )->escaped() . '</a></li>', + '<li><a href="#filelinks">' . wfMessage( 'imagelinks' )->escaped() . '</a></li>', + ); + if ( $metadata ) { + $r[] = '<li><a href="#metadata">' . wfMessage( 'metadata' )->escaped() . '</a></li>'; + } + + wfRunHooks( 'ImagePageShowTOC', array( $this, &$r ) ); + + return '<ul id="filetoc">' . implode( "\n", $r ) . '</ul>'; + } + + /** + * Make a table with metadata to be shown in the output page. + * + * @todo FIXME: Bad interface, see note on MediaHandler::formatMetadata(). + * + * @param array $metadata The array containing the Exif data + * @return string The metadata table. This is treated as Wikitext (!) + */ + protected function makeMetadataTable( $metadata ) { + $r = "<div class=\"mw-imagepage-section-metadata\">"; + $r .= wfMessage( 'metadata-help' )->plain(); + $r .= "<table id=\"mw_metadata\" class=\"mw_metadata\">\n"; + foreach ( $metadata as $type => $stuff ) { + foreach ( $stuff as $v ) { + # @todo FIXME: Why is this using escapeId for a class?! + $class = Sanitizer::escapeId( $v['id'] ); + if ( $type == 'collapsed' ) { + // Handled by mediawiki.action.view.metadata module + // and skins/common/shared.css. + $class .= ' collapsable'; + } + $r .= "<tr class=\"$class\">\n"; + $r .= "<th>{$v['name']}</th>\n"; + $r .= "<td>{$v['value']}</td>\n</tr>"; + } + } + $r .= "</table>\n</div>\n"; + return $r; + } + + /** + * Overloading Article's getContentObject method. + * + * Omit noarticletext if sharedupload; text will be fetched from the + * shared upload server if possible. + * @return string + */ + public function getContentObject() { + $this->loadFile(); + if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) { + return null; + } + return parent::getContentObject(); + } + + protected function openShowImage() { + global $wgImageLimits, $wgEnableUploads, $wgSend404Code; + + $this->loadFile(); + $out = $this->getContext()->getOutput(); + $user = $this->getContext()->getUser(); + $lang = $this->getContext()->getLanguage(); + $dirmark = $lang->getDirMarkEntity(); + $request = $this->getContext()->getRequest(); + + $max = $this->getImageLimitsFromOption( $user, 'imagesize' ); + $maxWidth = $max[0]; + $maxHeight = $max[1]; + + if ( $this->displayImg->exists() ) { + # image + $page = $request->getIntOrNull( 'page' ); + if ( is_null( $page ) ) { + $params = array(); + $page = 1; + } else { + $params = array( 'page' => $page ); + } + + $renderLang = $request->getVal( 'lang' ); + if ( !is_null( $renderLang ) ) { + $handler = $this->displayImg->getHandler(); + if ( $handler && $handler->validateParam( 'lang', $renderLang ) ) { + $params['lang'] = $renderLang; + } else { + $renderLang = null; + } + } + + $width_orig = $this->displayImg->getWidth( $page ); + $width = $width_orig; + $height_orig = $this->displayImg->getHeight( $page ); + $height = $height_orig; + + $filename = wfEscapeWikiText( $this->displayImg->getName() ); + $linktext = $filename; + + wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$out ) ); + + if ( $this->displayImg->allowInlineDisplay() ) { + # image + + # "Download high res version" link below the image + # $msgsize = wfMessage( 'file-info-size', $width_orig, $height_orig, + # Linker::formatSize( $this->displayImg->getSize() ), $mime )->escaped(); + # We'll show a thumbnail of this image + if ( $width > $maxWidth || $height > $maxHeight ) { + # Calculate the thumbnail size. + # First case, the limiting factor is the width, not the height. + /** @todo // FIXME: Possible division by 0. bug 36911 */ + if ( $width / $height >= $maxWidth / $maxHeight ) { + /** @todo // FIXME: Possible division by 0. bug 36911 */ + $height = round( $height * $maxWidth / $width ); + $width = $maxWidth; + # Note that $height <= $maxHeight now. + } else { + /** @todo // FIXME: Possible division by 0. bug 36911 */ + $newwidth = floor( $width * $maxHeight / $height ); + /** @todo // FIXME: Possible division by 0. bug 36911 */ + $height = round( $height * $newwidth / $width ); + $width = $newwidth; + # Note that $height <= $maxHeight now, but might not be identical + # because of rounding. + } + $linktext = wfMessage( 'show-big-image' )->escaped(); + if ( $this->displayImg->getRepo()->canTransformVia404() ) { + $thumbSizes = $wgImageLimits; + // Also include the full sized resolution in the list, so + // that users know they can get it. This will link to the + // original file asset if mustRender() === false. In the case + // that we mustRender, some users have indicated that they would + // find it useful to have the full size image in the rendered + // image format. + $thumbSizes[] = array( $width_orig, $height_orig ); + } else { + # Creating thumb links triggers thumbnail generation. + # Just generate the thumb for the current users prefs. + $thumbSizes = array( $this->getImageLimitsFromOption( $user, 'thumbsize' ) ); + if ( !$this->displayImg->mustRender() ) { + // We can safely include a link to the "full-size" preview, + // without actually rendering. + $thumbSizes[] = array( $width_orig, $height_orig ); + } + } + # Generate thumbnails or thumbnail links as needed... + $otherSizes = array(); + foreach ( $thumbSizes as $size ) { + // We include a thumbnail size in the list, if it is + // less than or equal to the original size of the image + // asset ($width_orig/$height_orig). We also exclude + // the current thumbnail's size ($width/$height) + // since that is added to the message separately, so + // it can be denoted as the current size being shown. + if ( $size[0] <= $width_orig && $size[1] <= $height_orig + && $size[0] != $width && $size[1] != $height + ) { + $sizeLink = $this->makeSizeLink( $params, $size[0], $size[1] ); + if ( $sizeLink ) { + $otherSizes[] = $sizeLink; + } + } + } + $otherSizes = array_unique( $otherSizes ); + $msgsmall = ''; + $sizeLinkBigImagePreview = $this->makeSizeLink( $params, $width, $height ); + if ( $sizeLinkBigImagePreview ) { + $msgsmall .= wfMessage( 'show-big-image-preview' )-> + rawParams( $sizeLinkBigImagePreview )-> + parse(); + } + if ( count( $otherSizes ) ) { + $msgsmall .= ' ' . + Html::rawElement( 'span', array( 'class' => 'mw-filepage-other-resolutions' ), + wfMessage( 'show-big-image-other' )->rawParams( $lang->pipeList( $otherSizes ) )-> + params( count( $otherSizes ) )->parse() + ); + } + } elseif ( $width == 0 && $height == 0 ) { + # Some sort of audio file that doesn't have dimensions + # Don't output a no hi res message for such a file + $msgsmall = ''; + } elseif ( $this->displayImg->isVectorized() ) { + # For vectorized images, full size is just the frame size + $msgsmall = ''; + } else { + # Image is small enough to show full size on image page + $msgsmall = wfMessage( 'file-nohires' )->parse(); + } + + $params['width'] = $width; + $params['height'] = $height; + $thumbnail = $this->displayImg->transform( $params ); + Linker::processResponsiveImages( $this->displayImg, $thumbnail, $params ); + + $anchorclose = Html::rawElement( + 'div', + array( 'class' => 'mw-filepage-resolutioninfo' ), + $msgsmall + ); + + $isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1; + if ( $isMulti ) { + $out->addModules( 'mediawiki.page.image.pagination' ); + $out->addHTML( '<table class="multipageimage"><tr><td>' ); + } + + if ( $thumbnail ) { + $options = array( + 'alt' => $this->displayImg->getTitle()->getPrefixedText(), + 'file-link' => true, + ); + $out->addHTML( '<div class="fullImageLink" id="file">' . + $thumbnail->toHtml( $options ) . + $anchorclose . "</div>\n" ); + } + + if ( $isMulti ) { + $count = $this->displayImg->pageCount(); + + if ( $page > 1 ) { + $label = $out->parse( wfMessage( 'imgmultipageprev' )->text(), false ); + $link = Linker::linkKnown( + $this->getTitle(), + $label, + array(), + array( 'page' => $page - 1 ) + ); + $thumb1 = Linker::makeThumbLinkObj( + $this->getTitle(), + $this->displayImg, + $link, + $label, + 'none', + array( 'page' => $page - 1 ) + ); + } else { + $thumb1 = ''; + } + + if ( $page < $count ) { + $label = wfMessage( 'imgmultipagenext' )->text(); + $link = Linker::linkKnown( + $this->getTitle(), + $label, + array(), + array( 'page' => $page + 1 ) + ); + $thumb2 = Linker::makeThumbLinkObj( + $this->getTitle(), + $this->displayImg, + $link, + $label, + 'none', + array( 'page' => $page + 1 ) + ); + } else { + $thumb2 = ''; + } + + global $wgScript; + + $formParams = array( + 'name' => 'pageselector', + 'action' => $wgScript, + ); + $options = array(); + for ( $i = 1; $i <= $count; $i++ ) { + $options[] = Xml::option( $lang->formatNum( $i ), $i, $i == $page ); + } + $select = Xml::tags( 'select', + array( 'id' => 'pageselector', 'name' => 'page' ), + implode( "\n", $options ) ); + + $out->addHTML( + '</td><td><div class="multipageimagenavbox">' . + Xml::openElement( 'form', $formParams ) . + Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . + wfMessage( 'imgmultigoto' )->rawParams( $select )->parse() . + Xml::submitButton( wfMessage( 'imgmultigo' )->text() ) . + Xml::closeElement( 'form' ) . + "<hr />$thumb1\n$thumb2<br style=\"clear: both\" /></div></td></tr></table>" + ); + } + } elseif ( $this->displayImg->isSafeFile() ) { + # if direct link is allowed but it's not a renderable image, show an icon. + $icon = $this->displayImg->iconThumb(); + + $out->addHTML( '<div class="fullImageLink" id="file">' . + $icon->toHtml( array( 'file-link' => true ) ) . + "</div>\n" ); + } + + $longDesc = wfMessage( 'parentheses', $this->displayImg->getLongDesc() )->text(); + + $medialink = "[[Media:$filename|$linktext]]"; + + if ( !$this->displayImg->isSafeFile() ) { + $warning = wfMessage( 'mediawarning' )->plain(); + // dirmark is needed here to separate the file name, which + // most likely ends in Latin characters, from the description, + // which may begin with the file type. In RTL environment + // this will get messy. + // The dirmark, however, must not be immediately adjacent + // to the filename, because it can get copied with it. + // See bug 25277. + // @codingStandardsIgnoreStart Ignore long line + $out->addWikiText( <<<EOT +<div class="fullMedia"><span class="dangerousLink">{$medialink}</span> $dirmark<span class="fileInfo">$longDesc</span></div> +<div class="mediaWarning">$warning</div> +EOT + ); + // @codingStandardsIgnoreEnd + } else { + $out->addWikiText( <<<EOT +<div class="fullMedia">{$medialink} {$dirmark}<span class="fileInfo">$longDesc</span> +</div> +EOT + ); + } + + $renderLangOptions = $this->displayImg->getAvailableLanguages(); + if ( count( $renderLangOptions ) >= 1 ) { + $currentLanguage = $renderLang; + $defaultLang = $this->displayImg->getDefaultRenderLanguage(); + if ( is_null( $currentLanguage ) ) { + $currentLanguage = $defaultLang; + } + $out->addHtml( $this->doRenderLangOpt( $renderLangOptions, $currentLanguage, $defaultLang ) ); + } + + // Add cannot animate thumbnail warning + if ( !$this->displayImg->canAnimateThumbIfAppropriate() ) { + // Include the extension so wiki admins can + // customize it on a per file-type basis + // (aka say things like use format X instead). + // additionally have a specific message for + // file-no-thumb-animation-gif + $ext = $this->displayImg->getExtension(); + $noAnimMesg = wfMessageFallback( + 'file-no-thumb-animation-' . $ext, + 'file-no-thumb-animation' + )->plain(); + + $out->addWikiText( <<<EOT +<div class="mw-noanimatethumb">{$noAnimMesg}</div> +EOT + ); + } + + if ( !$this->displayImg->isLocal() ) { + $this->printSharedImageText(); + } + } else { + # Image does not exist + if ( !$this->getID() ) { + # No article exists either + # Show deletion log to be consistent with normal articles + LogEventsList::showLogExtract( + $out, + array( 'delete', 'move' ), + $this->getTitle()->getPrefixedText(), + '', + array( 'lim' => 10, + 'conds' => array( "log_action != 'revision'" ), + 'showIfEmpty' => false, + 'msgKey' => array( 'moveddeleted-notice' ) + ) + ); + } + + if ( $wgEnableUploads && $user->isAllowed( 'upload' ) ) { + // Only show an upload link if the user can upload + $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); + $nofile = array( + 'filepage-nofile-link', + $uploadTitle->getFullURL( array( 'wpDestFile' => $this->mPage->getFile()->getName() ) ) + ); + } else { + $nofile = 'filepage-nofile'; + } + // Note, if there is an image description page, but + // no image, then this setRobotPolicy is overridden + // by Article::View(). + $out->setRobotPolicy( 'noindex,nofollow' ); + $out->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile ); + if ( !$this->getID() && $wgSend404Code ) { + // If there is no image, no shared image, and no description page, + // output a 404, to be consistent with articles. + $request->response()->header( 'HTTP/1.1 404 Not Found' ); + } + } + $out->setFileVersion( $this->displayImg ); + } + + /** + * Creates an thumbnail of specified size and returns an HTML link to it + * @param array $params Scaler parameters + * @param int $width + * @param int $height + * @return string + */ + private function makeSizeLink( $params, $width, $height ) { + $params['width'] = $width; + $params['height'] = $height; + $thumbnail = $this->displayImg->transform( $params ); + if ( $thumbnail && !$thumbnail->isError() ) { + return Html::rawElement( 'a', array( + 'href' => $thumbnail->getUrl(), + 'class' => 'mw-thumbnail-link' + ), wfMessage( 'show-big-image-size' )->numParams( + $thumbnail->getWidth(), $thumbnail->getHeight() + )->parse() ); + } else { + return ''; + } + } + + /** + * Show a notice that the file is from a shared repository + */ + protected function printSharedImageText() { + $out = $this->getContext()->getOutput(); + $this->loadFile(); + + $descUrl = $this->mPage->getFile()->getDescriptionUrl(); + $descText = $this->mPage->getFile()->getDescriptionText( $this->getContext()->getLanguage() ); + + /* Add canonical to head if there is no local page for this shared file */ + if ( $descUrl && $this->mPage->getID() == 0 ) { + $out->setCanonicalUrl( $descUrl ); + } + + $wrap = "<div class=\"sharedUploadNotice\">\n$1\n</div>\n"; + $repo = $this->mPage->getFile()->getRepo()->getDisplayName(); + + if ( $descUrl && $descText && wfMessage( 'sharedupload-desc-here' )->plain() !== '-' ) { + $out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-here', $repo, $descUrl ) ); + } elseif ( $descUrl && wfMessage( 'sharedupload-desc-there' )->plain() !== '-' ) { + $out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-there', $repo, $descUrl ) ); + } else { + $out->wrapWikiMsg( $wrap, array( 'sharedupload', $repo ), ''/*BACKCOMPAT*/ ); + } + + if ( $descText ) { + $this->mExtraDescription = $descText; + } + } + + public function getUploadUrl() { + $this->loadFile(); + $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); + return $uploadTitle->getFullURL( array( + 'wpDestFile' => $this->mPage->getFile()->getName(), + 'wpForReUpload' => 1 + ) ); + } + + /** + * Print out the various links at the bottom of the image page, e.g. reupload, + * external editing (and instructions link) etc. + */ + protected function uploadLinksBox() { + global $wgEnableUploads; + + if ( !$wgEnableUploads ) { + return; + } + + $this->loadFile(); + if ( !$this->mPage->getFile()->isLocal() ) { + return; + } + + $out = $this->getContext()->getOutput(); + $out->addHTML( "<ul>\n" ); + + # "Upload a new version of this file" link + $canUpload = $this->getTitle()->userCan( 'upload', $this->getContext()->getUser() ); + if ( $canUpload && UploadBase::userCanReUpload( + $this->getContext()->getUser(), + $this->mPage->getFile()->name ) + ) { + $ulink = Linker::makeExternalLink( + $this->getUploadUrl(), + wfMessage( 'uploadnewversion-linktext' )->text() + ); + $out->addHTML( "<li id=\"mw-imagepage-reupload-link\">" + . "<div class=\"plainlinks\">{$ulink}</div></li>\n" ); + } else { + $out->addHTML( "<li id=\"mw-imagepage-upload-disallowed\">" + . $this->getContext()->msg( 'upload-disallowed-here' )->escaped() . "</li>\n" ); + } + + $out->addHTML( "</ul>\n" ); + } + + /** + * For overloading + */ + protected function closeShowImage() { + } + + /** + * If the page we've just displayed is in the "Image" namespace, + * we follow it with an upload history of the image and its usage. + */ + protected function imageHistory() { + $this->loadFile(); + $out = $this->getContext()->getOutput(); + $pager = new ImageHistoryPseudoPager( $this ); + $out->addHTML( $pager->getBody() ); + $out->preventClickjacking( $pager->getPreventClickjacking() ); + + $this->mPage->getFile()->resetHistory(); // free db resources + + # Exist check because we don't want to show this on pages where an image + # doesn't exist along with the noimage message, that would suck. -ævar + if ( $this->mPage->getFile()->exists() ) { + $this->uploadLinksBox(); + } + } + + /** + * @param string $target + * @param int $limit + * @return ResultWrapper + */ + protected function queryImageLinks( $target, $limit ) { + $dbr = wfGetDB( DB_SLAVE ); + + return $dbr->select( + array( 'imagelinks', 'page' ), + array( 'page_namespace', 'page_title', 'il_to' ), + array( 'il_to' => $target, 'il_from = page_id' ), + __METHOD__, + array( 'LIMIT' => $limit + 1, 'ORDER BY' => 'il_from', ) + ); + } + + protected function imageLinks() { + $limit = 100; + + $out = $this->getContext()->getOutput(); + + $rows = array(); + $redirects = array(); + foreach ( $this->getTitle()->getRedirectsHere( NS_FILE ) as $redir ) { + $redirects[$redir->getDBkey()] = array(); + $rows[] = (object)array( + 'page_namespace' => NS_FILE, + 'page_title' => $redir->getDBkey(), + ); + } + + $res = $this->queryImageLinks( $this->getTitle()->getDBkey(), $limit + 1 ); + foreach ( $res as $row ) { + $rows[] = $row; + } + $count = count( $rows ); + + $hasMore = $count > $limit; + if ( !$hasMore && count( $redirects ) ) { + $res = $this->queryImageLinks( array_keys( $redirects ), + $limit - count( $rows ) + 1 ); + foreach ( $res as $row ) { + $redirects[$row->il_to][] = $row; + $count++; + } + $hasMore = ( $res->numRows() + count( $rows ) ) > $limit; + } + + if ( $count == 0 ) { + $out->wrapWikiMsg( + Html::rawElement( 'div', + array( 'id' => 'mw-imagepage-nolinkstoimage' ), "\n$1\n" ), + 'nolinkstoimage' + ); + return; + } + + $out->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" ); + if ( !$hasMore ) { + $out->addWikiMsg( 'linkstoimage', $count ); + } else { + // More links than the limit. Add a link to [[Special:Whatlinkshere]] + $out->addWikiMsg( 'linkstoimage-more', + $this->getContext()->getLanguage()->formatNum( $limit ), + $this->getTitle()->getPrefixedDBkey() + ); + } + + $out->addHTML( + Html::openElement( 'ul', + array( 'class' => 'mw-imagepage-linkstoimage' ) ) . "\n" + ); + $count = 0; + + // Sort the list by namespace:title + usort( $rows, array( $this, 'compare' ) ); + + // Create links for every element + $currentCount = 0; + foreach ( $rows as $element ) { + $currentCount++; + if ( $currentCount > $limit ) { + break; + } + + $query = array(); + # Add a redirect=no to make redirect pages reachable + if ( isset( $redirects[$element->page_title] ) ) { + $query['redirect'] = 'no'; + } + $link = Linker::linkKnown( + Title::makeTitle( $element->page_namespace, $element->page_title ), + null, array(), $query + ); + if ( !isset( $redirects[$element->page_title] ) ) { + # No redirects + $liContents = $link; + } elseif ( count( $redirects[$element->page_title] ) === 0 ) { + # Redirect without usages + $liContents = wfMessage( 'linkstoimage-redirect' )->rawParams( $link, '' )->parse(); + } else { + # Redirect with usages + $li = ''; + foreach ( $redirects[$element->page_title] as $row ) { + $currentCount++; + if ( $currentCount > $limit ) { + break; + } + + $link2 = Linker::linkKnown( Title::makeTitle( $row->page_namespace, $row->page_title ) ); + $li .= Html::rawElement( + 'li', + array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ), + $link2 + ) . "\n"; + } + + $ul = Html::rawElement( + 'ul', + array( 'class' => 'mw-imagepage-redirectstofile' ), + $li + ) . "\n"; + $liContents = wfMessage( 'linkstoimage-redirect' )->rawParams( + $link, $ul )->parse(); + } + $out->addHTML( Html::rawElement( + 'li', + array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ), + $liContents + ) . "\n" + ); + + }; + $out->addHTML( Html::closeElement( 'ul' ) . "\n" ); + $res->free(); + + // Add a links to [[Special:Whatlinkshere]] + if ( $count > $limit ) { + $out->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() ); + } + $out->addHTML( Html::closeElement( 'div' ) . "\n" ); + } + + protected function imageDupes() { + $this->loadFile(); + $out = $this->getContext()->getOutput(); + + $dupes = $this->mPage->getDuplicates(); + if ( count( $dupes ) == 0 ) { + return; + } + + $out->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" ); + $out->addWikiMsg( 'duplicatesoffile', + $this->getContext()->getLanguage()->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey() + ); + $out->addHTML( "<ul class='mw-imagepage-duplicates'>\n" ); + + /** + * @var $file File + */ + foreach ( $dupes as $file ) { + $fromSrc = ''; + if ( $file->isLocal() ) { + $link = Linker::linkKnown( $file->getTitle() ); + } else { + $link = Linker::makeExternalLink( $file->getDescriptionUrl(), + $file->getTitle()->getPrefixedText() ); + $fromSrc = wfMessage( 'shared-repo-from', $file->getRepo()->getDisplayName() )->text(); + } + $out->addHTML( "<li>{$link} {$fromSrc}</li>\n" ); + } + $out->addHTML( "</ul></div>\n" ); + } + + /** + * Delete the file, or an earlier version of it + */ + public function delete() { + $file = $this->mPage->getFile(); + if ( !$file->exists() || !$file->isLocal() || $file->getRedirected() ) { + // Standard article deletion + parent::delete(); + return; + } + + $deleter = new FileDeleteForm( $file ); + $deleter->execute(); + } + + /** + * Display an error with a wikitext description + * + * @param string $description + */ + function showError( $description ) { + $out = $this->getContext()->getOutput(); + $out->setPageTitle( wfMessage( 'internalerror' ) ); + $out->setRobotPolicy( 'noindex,nofollow' ); + $out->setArticleRelated( false ); + $out->enableClientCache( false ); + $out->addWikiText( $description ); + } + + /** + * Callback for usort() to do link sorts by (namespace, title) + * Function copied from Title::compare() + * + * @param object $a Object page to compare with + * @param object $b Object page to compare with + * @return int Result of string comparison, or namespace comparison + */ + protected function compare( $a, $b ) { + if ( $a->page_namespace == $b->page_namespace ) { + return strcmp( $a->page_title, $b->page_title ); + } else { + return $a->page_namespace - $b->page_namespace; + } + } + + /** + * Returns the corresponding $wgImageLimits entry for the selected user option + * + * @param User $user + * @param string $optionName Name of a option to check, typically imagesize or thumbsize + * @return array + * @since 1.21 + */ + public function getImageLimitsFromOption( $user, $optionName ) { + global $wgImageLimits; + + $option = $user->getIntOption( $optionName ); + if ( !isset( $wgImageLimits[$option] ) ) { + $option = User::getDefaultOption( $optionName ); + } + + // The user offset might still be incorrect, specially if + // $wgImageLimits got changed (see bug #8858). + if ( !isset( $wgImageLimits[$option] ) ) { + // Default to the first offset in $wgImageLimits + $option = 0; + } + + return isset( $wgImageLimits[$option] ) + ? $wgImageLimits[$option] + : array( 800, 600 ); // if nothing is set, fallback to a hardcoded default + } + + /** + * Output a drop-down box for language options for the file + * + * @param array $langChoices Array of string language codes + * @param string $curLang Language code file is being viewed in. + * @param string $defaultLang Language code that image is rendered in by default + * @return string HTML to insert underneath image. + */ + protected function doRenderLangOpt( array $langChoices, $curLang, $defaultLang ) { + global $wgScript; + sort( $langChoices ); + $curLang = wfBCP47( $curLang ); + $defaultLang = wfBCP47( $defaultLang ); + $opts = ''; + $haveCurrentLang = false; + $haveDefaultLang = false; + + // We make a list of all the language choices in the file. + // Additionally if the default language to render this file + // is not included as being in this file (for example, in svgs + // usually the fallback content is the english content) also + // include a choice for that. Last of all, if we're viewing + // the file in a language not on the list, add it as a choice. + foreach ( $langChoices as $lang ) { + $code = wfBCP47( $lang ); + $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() ); + if ( $name !== '' ) { + $display = wfMessage( 'img-lang-opt', $code, $name )->text(); + } else { + $display = $code; + } + $opts .= "\n" . Xml::option( $display, $code, $curLang === $code ); + if ( $curLang === $code ) { + $haveCurrentLang = true; + } + if ( $defaultLang === $code ) { + $haveDefaultLang = true; + } + } + if ( !$haveDefaultLang ) { + // Its hard to know if the content is really in the default language, or + // if its just unmarked content that could be in any language. + $opts = Xml::option( + wfMessage( 'img-lang-default' )->text(), + $defaultLang, + $defaultLang === $curLang + ) . $opts; + } + if ( !$haveCurrentLang && $defaultLang !== $curLang ) { + $name = Language::fetchLanguageName( $curLang, $this->getContext()->getLanguage()->getCode() ); + if ( $name !== '' ) { + $display = wfMessage( 'img-lang-opt', $curLang, $name )->text(); + } else { + $display = $curLang; + } + $opts = Xml::option( $display, $curLang, true ) . $opts; + } + + $select = Html::rawElement( + 'select', + array( 'id' => 'mw-imglangselector', 'name' => 'lang' ), + $opts + ); + $submit = Xml::submitButton( wfMessage( 'img-lang-go' )->text() ); + + $formContents = wfMessage( 'img-lang-info' )->rawParams( $select, $submit )->parse() + . Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ); + + $langSelectLine = Html::rawElement( 'div', array( 'id' => 'mw-imglangselector-line' ), + Html::rawElement( 'form', array( 'action' => $wgScript ), $formContents ) + ); + return $langSelectLine; + } +} + +/** + * Builds the image revision log shown on image pages + * + * @ingroup Media + */ +class ImageHistoryList extends ContextSource { + + /** + * @var Title + */ + protected $title; + + /** + * @var File + */ + protected $img; + + /** + * @var ImagePage + */ + protected $imagePage; + + /** + * @var File + */ + protected $current; + + protected $repo, $showThumb; + protected $preventClickjacking = false; + + /** + * @param ImagePage $imagePage + */ + public function __construct( $imagePage ) { + global $wgShowArchiveThumbnails; + $this->current = $imagePage->getFile(); + $this->img = $imagePage->getDisplayedFile(); + $this->title = $imagePage->getTitle(); + $this->imagePage = $imagePage; + $this->showThumb = $wgShowArchiveThumbnails && $this->img->canRender(); + $this->setContext( $imagePage->getContext() ); + } + + /** + * @return ImagePage + */ + public function getImagePage() { + return $this->imagePage; + } + + /** + * @return File + */ + public function getFile() { + return $this->img; + } + + /** + * @param string $navLinks + * @return string + */ + public function beginImageHistoryList( $navLinks = '' ) { + return Xml::element( 'h2', array( 'id' => 'filehistory' ), $this->msg( 'filehist' )->text() ) + . "\n" + . "<div id=\"mw-imagepage-section-filehistory\">\n" + . $this->msg( 'filehist-help' )->parseAsBlock() + . $navLinks . "\n" + . Xml::openElement( 'table', array( 'class' => 'wikitable filehistory' ) ) . "\n" + . '<tr><td></td>' + . ( $this->current->isLocal() + && ( $this->getUser()->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<td></td>' : '' ) + . '<th>' . $this->msg( 'filehist-datetime' )->escaped() . '</th>' + . ( $this->showThumb ? '<th>' . $this->msg( 'filehist-thumb' )->escaped() . '</th>' : '' ) + . '<th>' . $this->msg( 'filehist-dimensions' )->escaped() . '</th>' + . '<th>' . $this->msg( 'filehist-user' )->escaped() . '</th>' + . '<th>' . $this->msg( 'filehist-comment' )->escaped() . '</th>' + . "</tr>\n"; + } + + /** + * @param string $navLinks + * @return string + */ + public function endImageHistoryList( $navLinks = '' ) { + return "</table>\n$navLinks\n</div>\n"; + } + + /** + * @param bool $iscur + * @param File $file + * @return string + */ + public function imageHistoryLine( $iscur, $file ) { + global $wgContLang; + + $user = $this->getUser(); + $lang = $this->getLanguage(); + $timestamp = wfTimestamp( TS_MW, $file->getTimestamp() ); + $img = $iscur ? $file->getName() : $file->getArchiveName(); + $userId = $file->getUser( 'id' ); + $userText = $file->getUser( 'text' ); + $description = $file->getDescription( File::FOR_THIS_USER, $user ); + + $local = $this->current->isLocal(); + $row = $selected = ''; + + // Deletion link + if ( $local && ( $user->isAllowedAny( 'delete', 'deletedhistory' ) ) ) { + $row .= '<td>'; + # Link to remove from history + if ( $user->isAllowed( 'delete' ) ) { + $q = array( 'action' => 'delete' ); + if ( !$iscur ) { + $q['oldimage'] = $img; + } + $row .= Linker::linkKnown( + $this->title, + $this->msg( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' )->escaped(), + array(), $q + ); + } + # Link to hide content. Don't show useless link to people who cannot hide revisions. + $canHide = $user->isAllowed( 'deleterevision' ); + if ( $canHide || ( $user->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) { + if ( $user->isAllowed( 'delete' ) ) { + $row .= '<br />'; + } + // If file is top revision or locked from this user, don't link + if ( $iscur || !$file->userCan( File::DELETED_RESTRICTED, $user ) ) { + $del = Linker::revDeleteLinkDisabled( $canHide ); + } else { + list( $ts, ) = explode( '!', $img, 2 ); + $query = array( + 'type' => 'oldimage', + 'target' => $this->title->getPrefixedText(), + 'ids' => $ts, + ); + $del = Linker::revDeleteLink( $query, + $file->isDeleted( File::DELETED_RESTRICTED ), $canHide ); + } + $row .= $del; + } + $row .= '</td>'; + } + + // Reversion link/current indicator + $row .= '<td>'; + if ( $iscur ) { + $row .= $this->msg( 'filehist-current' )->escaped(); + } elseif ( $local && $this->title->quickUserCan( 'edit', $user ) + && $this->title->quickUserCan( 'upload', $user ) + ) { + if ( $file->isDeleted( File::DELETED_FILE ) ) { + $row .= $this->msg( 'filehist-revert' )->escaped(); + } else { + $row .= Linker::linkKnown( + $this->title, + $this->msg( 'filehist-revert' )->escaped(), + array(), + array( + 'action' => 'revert', + 'oldimage' => $img, + 'wpEditToken' => $user->getEditToken( $img ) + ) + ); + } + } + $row .= '</td>'; + + // Date/time and image link + if ( $file->getTimestamp() === $this->img->getTimestamp() ) { + $selected = "class='filehistory-selected'"; + } + $row .= "<td $selected style='white-space: nowrap;'>"; + if ( !$file->userCan( File::DELETED_FILE, $user ) ) { + # Don't link to unviewable files + $row .= '<span class="history-deleted">' + . $lang->userTimeAndDate( $timestamp, $user ) . '</span>'; + } elseif ( $file->isDeleted( File::DELETED_FILE ) ) { + if ( $local ) { + $this->preventClickjacking(); + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); + # Make a link to review the image + $url = Linker::linkKnown( + $revdel, + $lang->userTimeAndDate( $timestamp, $user ), + array(), + array( + 'target' => $this->title->getPrefixedText(), + 'file' => $img, + 'token' => $user->getEditToken( $img ) + ) + ); + } else { + $url = $lang->userTimeAndDate( $timestamp, $user ); + } + $row .= '<span class="history-deleted">' . $url . '</span>'; + } else { + $url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img ); + $row .= Xml::element( + 'a', + array( 'href' => $url ), + $lang->userTimeAndDate( $timestamp, $user ) + ); + } + $row .= "</td>"; + + // Thumbnail + if ( $this->showThumb ) { + $row .= '<td>' . $this->getThumbForLine( $file ) . '</td>'; + } + + // Image dimensions + size + $row .= '<td>'; + $row .= htmlspecialchars( $file->getDimensionsString() ); + $row .= $this->msg( 'word-separator' )->escaped(); + $row .= '<span style="white-space: nowrap;">'; + $row .= $this->msg( 'parentheses' )->sizeParams( $file->getSize() )->escaped(); + $row .= '</span>'; + $row .= '</td>'; + + // Uploading user + $row .= '<td>'; + // Hide deleted usernames + if ( $file->isDeleted( File::DELETED_USER ) ) { + $row .= '<span class="history-deleted">' + . $this->msg( 'rev-deleted-user' )->escaped() . '</span>'; + } else { + if ( $local ) { + $row .= Linker::userLink( $userId, $userText ); + $row .= $this->msg( 'word-separator' )->escaped(); + $row .= '<span style="white-space: nowrap;">'; + $row .= Linker::userToolLinks( $userId, $userText ); + $row .= '</span>'; + } else { + $row .= htmlspecialchars( $userText ); + } + } + $row .= '</td>'; + + // Don't show deleted descriptions + if ( $file->isDeleted( File::DELETED_COMMENT ) ) { + $row .= '<td><span class="history-deleted">' . + $this->msg( 'rev-deleted-comment' )->escaped() . '</span></td>'; + } else { + $row .= '<td dir="' . $wgContLang->getDir() . '">' . + Linker::formatComment( $description, $this->title ) . '</td>'; + } + + $rowClass = null; + wfRunHooks( 'ImagePageFileHistoryLine', array( $this, $file, &$row, &$rowClass ) ); + $classAttr = $rowClass ? " class='$rowClass'" : ''; + + return "<tr{$classAttr}>{$row}</tr>\n"; + } + + /** + * @param File $file + * @return string + */ + protected function getThumbForLine( $file ) { + $lang = $this->getLanguage(); + $user = $this->getUser(); + if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE, $user ) + && !$file->isDeleted( File::DELETED_FILE ) + ) { + $params = array( + 'width' => '120', + 'height' => '120', + ); + $timestamp = wfTimestamp( TS_MW, $file->getTimestamp() ); + + $thumbnail = $file->transform( $params ); + $options = array( + 'alt' => $this->msg( 'filehist-thumbtext', + $lang->userTimeAndDate( $timestamp, $user ), + $lang->userDate( $timestamp, $user ), + $lang->userTime( $timestamp, $user ) )->text(), + 'file-link' => true, + ); + + if ( !$thumbnail ) { + return $this->msg( 'filehist-nothumb' )->escaped(); + } + + return $thumbnail->toHtml( $options ); + } else { + return $this->msg( 'filehist-nothumb' )->escaped(); + } + } + + /** + * @param bool $enable + */ + protected function preventClickjacking( $enable = true ) { + $this->preventClickjacking = $enable; + } + + /** + * @return bool + */ + public function getPreventClickjacking() { + return $this->preventClickjacking; + } +} + +class ImageHistoryPseudoPager extends ReverseChronologicalPager { + protected $preventClickjacking = false; + + /** + * @var File + */ + protected $mImg; + + /** + * @var Title + */ + protected $mTitle; + + /** + * @param ImagePage $imagePage + */ + function __construct( $imagePage ) { + parent::__construct( $imagePage->getContext() ); + $this->mImagePage = $imagePage; + $this->mTitle = clone ( $imagePage->getTitle() ); + $this->mTitle->setFragment( '#filehistory' ); + $this->mImg = null; + $this->mHist = array(); + $this->mRange = array( 0, 0 ); // display range + } + + /** + * @return Title + */ + function getTitle() { + return $this->mTitle; + } + + function getQueryInfo() { + return false; + } + + /** + * @return string + */ + function getIndexField() { + return ''; + } + + /** + * @param object $row + * @return string + */ + function formatRow( $row ) { + return ''; + } + + /** + * @return string + */ + function getBody() { + $s = ''; + $this->doQuery(); + if ( count( $this->mHist ) ) { + $list = new ImageHistoryList( $this->mImagePage ); + # Generate prev/next links + $navLink = $this->getNavigationBar(); + $s = $list->beginImageHistoryList( $navLink ); + // Skip rows there just for paging links + for ( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) { + $file = $this->mHist[$i]; + $s .= $list->imageHistoryLine( !$file->isOld(), $file ); + } + $s .= $list->endImageHistoryList( $navLink ); + + if ( $list->getPreventClickjacking() ) { + $this->preventClickjacking(); + } + } + return $s; + } + + function doQuery() { + if ( $this->mQueryDone ) { + return; + } + $this->mImg = $this->mImagePage->getFile(); // ensure loading + if ( !$this->mImg->exists() ) { + return; + } + $queryLimit = $this->mLimit + 1; // limit plus extra row + if ( $this->mIsBackwards ) { + // Fetch the file history + $this->mHist = $this->mImg->getHistory( $queryLimit, null, $this->mOffset, false ); + // The current rev may not meet the offset/limit + $numRows = count( $this->mHist ); + if ( $numRows <= $this->mLimit && $this->mImg->getTimestamp() > $this->mOffset ) { + $this->mHist = array_merge( array( $this->mImg ), $this->mHist ); + } + } else { + // The current rev may not meet the offset + if ( !$this->mOffset || $this->mImg->getTimestamp() < $this->mOffset ) { + $this->mHist[] = $this->mImg; + } + // Old image versions (fetch extra row for nav links) + $oiLimit = count( $this->mHist ) ? $this->mLimit : $this->mLimit + 1; + // Fetch the file history + $this->mHist = array_merge( $this->mHist, + $this->mImg->getHistory( $oiLimit, $this->mOffset, null, false ) ); + } + $numRows = count( $this->mHist ); // Total number of query results + if ( $numRows ) { + # Index value of top item in the list + $firstIndex = $this->mIsBackwards ? + $this->mHist[$numRows - 1]->getTimestamp() : $this->mHist[0]->getTimestamp(); + # Discard the extra result row if there is one + if ( $numRows > $this->mLimit && $numRows > 1 ) { + if ( $this->mIsBackwards ) { + # Index value of item past the index + $this->mPastTheEndIndex = $this->mHist[0]->getTimestamp(); + # Index value of bottom item in the list + $lastIndex = $this->mHist[1]->getTimestamp(); + # Display range + $this->mRange = array( 1, $numRows - 1 ); + } else { + # Index value of item past the index + $this->mPastTheEndIndex = $this->mHist[$numRows - 1]->getTimestamp(); + # Index value of bottom item in the list + $lastIndex = $this->mHist[$numRows - 2]->getTimestamp(); + # Display range + $this->mRange = array( 0, $numRows - 2 ); + } + } else { + # Setting indexes to an empty string means that they will be + # omitted if they would otherwise appear in URLs. It just so + # happens that this is the right thing to do in the standard + # UI, in all the relevant cases. + $this->mPastTheEndIndex = ''; + # Index value of bottom item in the list + $lastIndex = $this->mIsBackwards ? + $this->mHist[0]->getTimestamp() : $this->mHist[$numRows - 1]->getTimestamp(); + # Display range + $this->mRange = array( 0, $numRows - 1 ); + } + } else { + $firstIndex = ''; + $lastIndex = ''; + $this->mPastTheEndIndex = ''; + } + if ( $this->mIsBackwards ) { + $this->mIsFirst = ( $numRows < $queryLimit ); + $this->mIsLast = ( $this->mOffset == '' ); + $this->mLastShown = $firstIndex; + $this->mFirstShown = $lastIndex; + } else { + $this->mIsFirst = ( $this->mOffset == '' ); + $this->mIsLast = ( $numRows < $queryLimit ); + $this->mLastShown = $lastIndex; + $this->mFirstShown = $firstIndex; + } + $this->mQueryDone = true; + } + + /** + * @param bool $enable + */ + protected function preventClickjacking( $enable = true ) { + $this->preventClickjacking = $enable; + } + + /** + * @return bool + */ + public function getPreventClickjacking() { + return $this->preventClickjacking; + } + +} diff --git a/includes/page/WikiCategoryPage.php b/includes/page/WikiCategoryPage.php new file mode 100644 index 0000000000..d38200169b --- /dev/null +++ b/includes/page/WikiCategoryPage.php @@ -0,0 +1,50 @@ +<?php +/** + * Special handling for category pages. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Special handling for category pages + */ +class WikiCategoryPage extends WikiPage { + + /** + * Don't return a 404 for categories in use. + * In use defined as: either the actual page exists + * or the category currently has members. + * + * @return bool + */ + public function hasViewableContent() { + if ( parent::hasViewableContent() ) { + return true; + } else { + $cat = Category::newFromTitle( $this->mTitle ); + // If any of these are not 0, then has members + if ( $cat->getPageCount() + || $cat->getSubcatCount() + || $cat->getFileCount() + ) { + return true; + } + } + return false; + } +} diff --git a/includes/page/WikiFilePage.php b/includes/page/WikiFilePage.php new file mode 100644 index 0000000000..34f15c3aa6 --- /dev/null +++ b/includes/page/WikiFilePage.php @@ -0,0 +1,236 @@ +<?php +/** + * Special handling for file pages. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Special handling for file pages + * + * @ingroup Media + */ +class WikiFilePage extends WikiPage { + /** + * @var File + */ + protected $mFile = false; // !< File object + protected $mRepo = null; // !< + protected $mFileLoaded = false; // !< + protected $mDupes = null; // !< + + public function __construct( $title ) { + parent::__construct( $title ); + $this->mDupes = null; + $this->mRepo = null; + } + + public function getActionOverrides() { + $overrides = parent::getActionOverrides(); + $overrides['revert'] = 'RevertFileAction'; + return $overrides; + } + + /** + * @param File $file + */ + public function setFile( $file ) { + $this->mFile = $file; + $this->mFileLoaded = true; + } + + /** + * @return bool + */ + protected function loadFile() { + if ( $this->mFileLoaded ) { + return true; + } + $this->mFileLoaded = true; + + $this->mFile = wfFindFile( $this->mTitle ); + if ( !$this->mFile ) { + $this->mFile = wfLocalFile( $this->mTitle ); // always a File + } + $this->mRepo = $this->mFile->getRepo(); + return true; + } + + /** + * @return mixed|null|Title + */ + public function getRedirectTarget() { + $this->loadFile(); + if ( $this->mFile->isLocal() ) { + return parent::getRedirectTarget(); + } + // Foreign image page + $from = $this->mFile->getRedirected(); + $to = $this->mFile->getName(); + if ( $from == $to ) { + return null; + } + $this->mRedirectTarget = Title::makeTitle( NS_FILE, $to ); + return $this->mRedirectTarget; + } + + /** + * @return bool|mixed|Title + */ + public function followRedirect() { + $this->loadFile(); + if ( $this->mFile->isLocal() ) { + return parent::followRedirect(); + } + $from = $this->mFile->getRedirected(); + $to = $this->mFile->getName(); + if ( $from == $to ) { + return false; + } + return Title::makeTitle( NS_FILE, $to ); + } + + /** + * @return bool + */ + public function isRedirect() { + $this->loadFile(); + if ( $this->mFile->isLocal() ) { + return parent::isRedirect(); + } + + return (bool)$this->mFile->getRedirected(); + } + + /** + * @return bool + */ + public function isLocal() { + $this->loadFile(); + return $this->mFile->isLocal(); + } + + /** + * @return bool|File + */ + public function getFile() { + $this->loadFile(); + return $this->mFile; + } + + /** + * @return array|null + */ + public function getDuplicates() { + $this->loadFile(); + if ( !is_null( $this->mDupes ) ) { + return $this->mDupes; + } + $hash = $this->mFile->getSha1(); + if ( !( $hash ) ) { + $this->mDupes = array(); + return $this->mDupes; + } + $dupes = RepoGroup::singleton()->findBySha1( $hash ); + // Remove duplicates with self and non matching file sizes + $self = $this->mFile->getRepoName() . ':' . $this->mFile->getName(); + $size = $this->mFile->getSize(); + + /** + * @var $file File + */ + foreach ( $dupes as $index => $file ) { + $key = $file->getRepoName() . ':' . $file->getName(); + if ( $key == $self ) { + unset( $dupes[$index] ); + } + if ( $file->getSize() != $size ) { + unset( $dupes[$index] ); + } + } + $this->mDupes = $dupes; + return $this->mDupes; + } + + /** + * Override handling of action=purge + * @return bool + */ + public function doPurge() { + $this->loadFile(); + if ( $this->mFile->exists() ) { + wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" ); + $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ); + $update->doUpdate(); + $this->mFile->upgradeRow(); + $this->mFile->purgeCache( array( 'forThumbRefresh' => true ) ); + } else { + wfDebug( 'ImagePage::doPurge no image for ' + . $this->mFile->getName() . "; limiting purge to cache only\n" ); + // even if the file supposedly doesn't exist, force any cached information + // to be updated (in case the cached information is wrong) + $this->mFile->purgeCache( array( 'forThumbRefresh' => true ) ); + } + if ( $this->mRepo ) { + // Purge redirect cache + $this->mRepo->invalidateImageRedirect( $this->mTitle ); + } + return parent::doPurge(); + } + + /** + * Get the categories this file is a member of on the wiki where it was uploaded. + * For local files, this is the same as getCategories(). + * For foreign API files (InstantCommons), this is not supported currently. + * Results will include hidden categories. + * + * @return TitleArray|Title[] + * @since 1.23 + */ + public function getForeignCategories() { + $this->loadFile(); + $title = $this->mTitle; + $file = $this->mFile; + + if ( ! $file instanceof LocalFile ) { + wfDebug( __CLASS__ . '::' . __METHOD__ . " is not supported for this file\n" ); + return TitleArray::newFromResult( new FakeResultWrapper( array() ) ); + } + + /** @var LocalRepo $repo */ + $repo = $file->getRepo(); + $dbr = $repo->getSlaveDB(); + + $res = $dbr->select( + array( 'page', 'categorylinks' ), + array( + 'page_title' => 'cl_to', + 'page_namespace' => NS_CATEGORY, + ), + array( + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDBkey(), + ), + __METHOD__, + array(), + array( 'categorylinks' => array( 'INNER JOIN', 'page_id = cl_from' ) ) + ); + + return TitleArray::newFromResult( $res ); + } +} diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php new file mode 100644 index 0000000000..855de8eb58 --- /dev/null +++ b/includes/page/WikiPage.php @@ -0,0 +1,3570 @@ +<?php +/** + * Base representation for a MediaWiki page. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage) + */ +interface Page { +} + +/** + * Class representing a MediaWiki article and history. + * + * Some fields are public only for backwards-compatibility. Use accessors. + * In the past, this class was part of Article.php and everything was public. + * + * @internal documentation reviewed 15 Mar 2010 + */ +class WikiPage implements Page, IDBAccessObject { + // Constants for $mDataLoadedFrom and related + + /** + * @var Title + */ + public $mTitle = null; + + /**@{{ + * @protected + */ + public $mDataLoaded = false; // !< Boolean + public $mIsRedirect = false; // !< Boolean + public $mLatest = false; // !< Integer (false means "not loaded") + /**@}}*/ + + /** @var stdclass Map of cache fields (text, parser output, ect) for a proposed/new edit */ + public $mPreparedEdit = false; + + /** + * @var int + */ + protected $mId = null; + + /** + * @var int One of the READ_* constants + */ + protected $mDataLoadedFrom = self::READ_NONE; + + /** + * @var Title + */ + protected $mRedirectTarget = null; + + /** + * @var Revision + */ + protected $mLastRevision = null; + + /** + * @var string Timestamp of the current revision or empty string if not loaded + */ + protected $mTimestamp = ''; + + /** + * @var string + */ + protected $mTouched = '19700101000000'; + + /** + * @var string + */ + protected $mLinksUpdated = '19700101000000'; + + /** + * @var int|null + */ + protected $mCounter = null; + + /** + * Constructor and clear the article + * @param Title $title Reference to a Title object. + */ + public function __construct( Title $title ) { + $this->mTitle = $title; + } + + /** + * Create a WikiPage object of the appropriate class for the given title. + * + * @param Title $title + * + * @throws MWException + * @return WikiPage Object of the appropriate type + */ + public static function factory( Title $title ) { + $ns = $title->getNamespace(); + + if ( $ns == NS_MEDIA ) { + throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." ); + } elseif ( $ns < 0 ) { + throw new MWException( "Invalid or virtual namespace $ns given." ); + } + + switch ( $ns ) { + case NS_FILE: + $page = new WikiFilePage( $title ); + break; + case NS_CATEGORY: + $page = new WikiCategoryPage( $title ); + break; + default: + $page = new WikiPage( $title ); + } + + return $page; + } + + /** + * Constructor from a page id + * + * @param int $id Article ID to load + * @param string|int $from One of the following values: + * - "fromdb" or WikiPage::READ_NORMAL to select from a slave database + * - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database + * + * @return WikiPage|null + */ + public static function newFromID( $id, $from = 'fromdb' ) { + // page id's are never 0 or negative, see bug 61166 + if ( $id < 1 ) { + return null; + } + + $from = self::convertSelectType( $from ); + $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE ); + $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ ); + if ( !$row ) { + return null; + } + return self::newFromRow( $row, $from ); + } + + /** + * Constructor from a database row + * + * @since 1.20 + * @param object $row Database row containing at least fields returned by selectFields(). + * @param string|int $from Source of $data: + * - "fromdb" or WikiPage::READ_NORMAL: from a slave DB + * - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB + * - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE + * @return WikiPage + */ + public static function newFromRow( $row, $from = 'fromdb' ) { + $page = self::factory( Title::newFromRow( $row ) ); + $page->loadFromRow( $row, $from ); + return $page; + } + + /** + * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants. + * + * @param object|string|int $type + * @return mixed + */ + private static function convertSelectType( $type ) { + switch ( $type ) { + case 'fromdb': + return self::READ_NORMAL; + case 'fromdbmaster': + return self::READ_LATEST; + case 'forupdate': + return self::READ_LOCKING; + default: + // It may already be an integer or whatever else + return $type; + } + } + + /** + * Returns overrides for action handlers. + * Classes listed here will be used instead of the default one when + * (and only when) $wgActions[$action] === true. This allows subclasses + * to override the default behavior. + * + * @todo Move this UI stuff somewhere else + * + * @return array + */ + public function getActionOverrides() { + $content_handler = $this->getContentHandler(); + return $content_handler->getActionOverrides(); + } + + /** + * Returns the ContentHandler instance to be used to deal with the content of this WikiPage. + * + * Shorthand for ContentHandler::getForModelID( $this->getContentModel() ); + * + * @return ContentHandler + * + * @since 1.21 + */ + public function getContentHandler() { + return ContentHandler::getForModelID( $this->getContentModel() ); + } + + /** + * Get the title object of the article + * @return Title Title object of this page + */ + public function getTitle() { + return $this->mTitle; + } + + /** + * Clear the object + * @return void + */ + public function clear() { + $this->mDataLoaded = false; + $this->mDataLoadedFrom = self::READ_NONE; + + $this->clearCacheFields(); + } + + /** + * Clear the object cache fields + * @return void + */ + protected function clearCacheFields() { + $this->mId = null; + $this->mCounter = null; + $this->mRedirectTarget = null; // Title object if set + $this->mLastRevision = null; // Latest revision + $this->mTouched = '19700101000000'; + $this->mLinksUpdated = '19700101000000'; + $this->mTimestamp = ''; + $this->mIsRedirect = false; + $this->mLatest = false; + // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks + // the requested rev ID and content against the cached one for equality. For most + // content types, the output should not change during the lifetime of this cache. + // Clearing it can cause extra parses on edit for no reason. + } + + /** + * Clear the mPreparedEdit cache field, as may be needed by mutable content types + * @return void + * @since 1.23 + */ + public function clearPreparedEdit() { + $this->mPreparedEdit = false; + } + + /** + * Return the list of revision fields that should be selected to create + * a new page. + * + * @return array + */ + public static function selectFields() { + global $wgContentHandlerUseDB; + + $fields = array( + 'page_id', + 'page_namespace', + 'page_title', + 'page_restrictions', + 'page_counter', + 'page_is_redirect', + 'page_is_new', + 'page_random', + 'page_touched', + 'page_links_updated', + 'page_latest', + 'page_len', + ); + + if ( $wgContentHandlerUseDB ) { + $fields[] = 'page_content_model'; + } + + return $fields; + } + + /** + * Fetch a page record with the given conditions + * @param DatabaseBase $dbr + * @param array $conditions + * @param array $options + * @return object|bool Database result resource, or false on failure + */ + protected function pageData( $dbr, $conditions, $options = array() ) { + $fields = self::selectFields(); + + wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); + + $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options ); + + wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); + + return $row; + } + + /** + * Fetch a page record matching the Title object's namespace and title + * using a sanitized title string + * + * @param DatabaseBase $dbr + * @param Title $title + * @param array $options + * @return object|bool Database result resource, or false on failure + */ + public function pageDataFromTitle( $dbr, $title, $options = array() ) { + return $this->pageData( $dbr, array( + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDBkey() ), $options ); + } + + /** + * Fetch a page record matching the requested ID + * + * @param DatabaseBase $dbr + * @param int $id + * @param array $options + * @return object|bool Database result resource, or false on failure + */ + public function pageDataFromId( $dbr, $id, $options = array() ) { + return $this->pageData( $dbr, array( 'page_id' => $id ), $options ); + } + + /** + * Set the general counter, title etc data loaded from + * some source. + * + * @param object|string|int $from One of the following: + * - A DB query result object. + * - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB. + * - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB. + * - "forupdate" or WikiPage::READ_LOCKING to get from the master DB + * using SELECT FOR UPDATE. + * + * @return void + */ + public function loadPageData( $from = 'fromdb' ) { + $from = self::convertSelectType( $from ); + if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) { + // We already have the data from the correct location, no need to load it twice. + return; + } + + if ( $from === self::READ_LOCKING ) { + $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) ); + } elseif ( $from === self::READ_LATEST ) { + $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); + } elseif ( $from === self::READ_NORMAL ) { + $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); + // Use a "last rev inserted" timestamp key to diminish the issue of slave lag. + // Note that DB also stores the master position in the session and checks it. + $touched = $this->getCachedLastEditTime(); + if ( $touched ) { // key set + if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) { + $from = self::READ_LATEST; + $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); + } + } + } else { + // No idea from where the caller got this data, assume slave database. + $data = $from; + $from = self::READ_NORMAL; + } + + $this->loadFromRow( $data, $from ); + } + + /** + * Load the object from a database row + * + * @since 1.20 + * @param object $data Database row containing at least fields returned by selectFields() + * @param string|int $from One of the following: + * - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB + * - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB + * - "forupdate" or WikiPage::READ_LOCKING if the data comes from from + * the master DB using SELECT FOR UPDATE + */ + public function loadFromRow( $data, $from ) { + $lc = LinkCache::singleton(); + $lc->clearLink( $this->mTitle ); + + if ( $data ) { + $lc->addGoodLinkObjFromRow( $this->mTitle, $data ); + + $this->mTitle->loadFromRow( $data ); + + // Old-fashioned restrictions + $this->mTitle->loadRestrictions( $data->page_restrictions ); + + $this->mId = intval( $data->page_id ); + $this->mCounter = intval( $data->page_counter ); + $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); + $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated ); + $this->mIsRedirect = intval( $data->page_is_redirect ); + $this->mLatest = intval( $data->page_latest ); + // Bug 37225: $latest may no longer match the cached latest Revision object. + // Double-check the ID of any cached latest Revision object for consistency. + if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) { + $this->mLastRevision = null; + $this->mTimestamp = ''; + } + } else { + $lc->addBadLinkObj( $this->mTitle ); + + $this->mTitle->loadFromRow( false ); + + $this->clearCacheFields(); + + $this->mId = 0; + } + + $this->mDataLoaded = true; + $this->mDataLoadedFrom = self::convertSelectType( $from ); + } + + /** + * @return int Page ID + */ + public function getId() { + if ( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return $this->mId; + } + + /** + * @return bool Whether or not the page exists in the database + */ + public function exists() { + if ( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return $this->mId > 0; + } + + /** + * Check if this page is something we're going to be showing + * some sort of sensible content for. If we return false, page + * views (plain action=view) will return an HTTP 404 response, + * so spiders and robots can know they're following a bad link. + * + * @return bool + */ + public function hasViewableContent() { + return $this->exists() || $this->mTitle->isAlwaysKnown(); + } + + /** + * @return int The view count for the page + */ + public function getCount() { + if ( !$this->mDataLoaded ) { + $this->loadPageData(); + } + + return $this->mCounter; + } + + /** + * Tests if the article content represents a redirect + * + * @return bool + */ + public function isRedirect() { + $content = $this->getContent(); + if ( !$content ) { + return false; + } + + return $content->isRedirect(); + } + + /** + * Returns the page's content model id (see the CONTENT_MODEL_XXX constants). + * + * Will use the revisions actual content model if the page exists, + * and the page's default if the page doesn't exist yet. + * + * @return string + * + * @since 1.21 + */ + public function getContentModel() { + if ( $this->exists() ) { + // look at the revision's actual content model + $rev = $this->getRevision(); + + if ( $rev !== null ) { + return $rev->getContentModel(); + } else { + $title = $this->mTitle->getPrefixedDBkey(); + wfWarn( "Page $title exists but has no (visible) revisions!" ); + } + } + + // use the default model for this page + return $this->mTitle->getContentModel(); + } + + /** + * Loads page_touched and returns a value indicating if it should be used + * @return bool true if not a redirect + */ + public function checkTouched() { + if ( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return !$this->mIsRedirect; + } + + /** + * Get the page_touched field + * @return string Containing GMT timestamp + */ + public function getTouched() { + if ( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return $this->mTouched; + } + + /** + * Get the page_links_updated field + * @return string|null Containing GMT timestamp + */ + public function getLinksTimestamp() { + if ( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return $this->mLinksUpdated; + } + + /** + * Get the page_latest field + * @return int rev_id of current revision + */ + public function getLatest() { + if ( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return (int)$this->mLatest; + } + + /** + * Get the Revision object of the oldest revision + * @return Revision|null + */ + public function getOldestRevision() { + wfProfileIn( __METHOD__ ); + + // Try using the slave database first, then try the master + $continue = 2; + $db = wfGetDB( DB_SLAVE ); + $revSelectFields = Revision::selectFields(); + + $row = null; + while ( $continue ) { + $row = $db->selectRow( + array( 'page', 'revision' ), + $revSelectFields, + array( + 'page_namespace' => $this->mTitle->getNamespace(), + 'page_title' => $this->mTitle->getDBkey(), + 'rev_page = page_id' + ), + __METHOD__, + array( + 'ORDER BY' => 'rev_timestamp ASC' + ) + ); + + if ( $row ) { + $continue = 0; + } else { + $db = wfGetDB( DB_MASTER ); + $continue--; + } + } + + wfProfileOut( __METHOD__ ); + return $row ? Revision::newFromRow( $row ) : null; + } + + /** + * Loads everything except the text + * This isn't necessary for all uses, so it's only done if needed. + */ + protected function loadLastEdit() { + if ( $this->mLastRevision !== null ) { + return; // already loaded + } + + $latest = $this->getLatest(); + if ( !$latest ) { + return; // page doesn't exist or is missing page_latest info + } + + // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the + // latest changes committed. This is true even within REPEATABLE-READ transactions, where + // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to + // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row + // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT. + // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read. + $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0; + $revision = Revision::newFromPageId( $this->getId(), $latest, $flags ); + if ( $revision ) { // sanity + $this->setLastEdit( $revision ); + } + } + + /** + * Set the latest revision + * @param Revision $revision + */ + protected function setLastEdit( Revision $revision ) { + $this->mLastRevision = $revision; + $this->mTimestamp = $revision->getTimestamp(); + } + + /** + * Get the latest revision + * @return Revision|null + */ + public function getRevision() { + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision; + } + return null; + } + + /** + * Get the content of the current revision. No side-effects... + * + * @param int $audience int One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * @param User $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return Content|null The content of the current revision + * + * @since 1.21 + */ + public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) { + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision->getContent( $audience, $user ); + } + return null; + } + + /** + * Get the text of the current revision. No side-effects... + * + * @param int $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to the given user + * Revision::RAW get the text regardless of permissions + * @param User $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return string|bool The text of the current revision + * @deprecated since 1.21, getContent() should be used instead. + */ + public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision->getText( $audience, $user ); + } + return false; + } + + /** + * Get the text of the current revision. No side-effects... + * + * @return string|bool The text of the current revision. False on failure + * @deprecated since 1.21, getContent() should be used instead. + */ + public function getRawText() { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + return $this->getText( Revision::RAW ); + } + + /** + * @return string MW timestamp of last article revision + */ + public function getTimestamp() { + // Check if the field has been filled by WikiPage::setTimestamp() + if ( !$this->mTimestamp ) { + $this->loadLastEdit(); + } + + return wfTimestamp( TS_MW, $this->mTimestamp ); + } + + /** + * Set the page timestamp (use only to avoid DB queries) + * @param string $ts MW timestamp of last article revision + * @return void + */ + public function setTimestamp( $ts ) { + $this->mTimestamp = wfTimestamp( TS_MW, $ts ); + } + + /** + * @param int $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to the given user + * Revision::RAW get the text regardless of permissions + * @param User $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return int user ID for the user that made the last article revision + */ + public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) { + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision->getUser( $audience, $user ); + } else { + return -1; + } + } + + /** + * Get the User object of the user who created the page + * @param int $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to the given user + * Revision::RAW get the text regardless of permissions + * @param User $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return User|null + */ + public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) { + $revision = $this->getOldestRevision(); + if ( $revision ) { + $userName = $revision->getUserText( $audience, $user ); + return User::newFromName( $userName, false ); + } else { + return null; + } + } + + /** + * @param int $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to the given user + * Revision::RAW get the text regardless of permissions + * @param User $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return string username of the user that made the last article revision + */ + public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) { + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision->getUserText( $audience, $user ); + } else { + return ''; + } + } + + /** + * @param int $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to the given user + * Revision::RAW get the text regardless of permissions + * @param User $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return string Comment stored for the last article revision + */ + public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) { + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision->getComment( $audience, $user ); + } else { + return ''; + } + } + + /** + * Returns true if last revision was marked as "minor edit" + * + * @return bool Minor edit indicator for the last article revision. + */ + public function getMinorEdit() { + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision->isMinor(); + } else { + return false; + } + } + + /** + * Get the cached timestamp for the last time the page changed. + * This is only used to help handle slave lag by comparing to page_touched. + * @return string MW timestamp + */ + protected function getCachedLastEditTime() { + global $wgMemc; + $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); + return $wgMemc->get( $key ); + } + + /** + * Set the cached timestamp for the last time the page changed. + * This is only used to help handle slave lag by comparing to page_touched. + * @param string $timestamp + * @return void + */ + public function setCachedLastEditTime( $timestamp ) { + global $wgMemc; + $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); + $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60 * 15 ); + } + + /** + * Determine whether a page would be suitable for being counted as an + * article in the site_stats table based on the title & its content + * + * @param object|bool $editInfo (false): object returned by prepareTextForEdit(), + * if false, the current database state will be used + * @return bool + */ + public function isCountable( $editInfo = false ) { + global $wgArticleCountMethod; + + if ( !$this->mTitle->isContentPage() ) { + return false; + } + + if ( $editInfo ) { + $content = $editInfo->pstContent; + } else { + $content = $this->getContent(); + } + + if ( !$content || $content->isRedirect() ) { + return false; + } + + $hasLinks = null; + + if ( $wgArticleCountMethod === 'link' ) { + // nasty special case to avoid re-parsing to detect links + + if ( $editInfo ) { + // ParserOutput::getLinks() is a 2D array of page links, so + // to be really correct we would need to recurse in the array + // but the main array should only have items in it if there are + // links. + $hasLinks = (bool)count( $editInfo->output->getLinks() ); + } else { + $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, + array( 'pl_from' => $this->getId() ), __METHOD__ ); + } + } + + return $content->isCountable( $hasLinks ); + } + + /** + * If this page is a redirect, get its target + * + * The target will be fetched from the redirect table if possible. + * If this page doesn't have an entry there, call insertRedirect() + * @return Title|null Title object, or null if this page is not a redirect + */ + public function getRedirectTarget() { + if ( !$this->mTitle->isRedirect() ) { + return null; + } + + if ( $this->mRedirectTarget !== null ) { + return $this->mRedirectTarget; + } + + // Query the redirect table + $dbr = wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow( 'redirect', + array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ), + array( 'rd_from' => $this->getId() ), + __METHOD__ + ); + + // rd_fragment and rd_interwiki were added later, populate them if empty + if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) { + $this->mRedirectTarget = Title::makeTitle( + $row->rd_namespace, $row->rd_title, + $row->rd_fragment, $row->rd_interwiki ); + return $this->mRedirectTarget; + } + + // This page doesn't have an entry in the redirect table + $this->mRedirectTarget = $this->insertRedirect(); + return $this->mRedirectTarget; + } + + /** + * Insert an entry for this page into the redirect table. + * + * Don't call this function directly unless you know what you're doing. + * @return Title|null Title object or null if not a redirect + */ + public function insertRedirect() { + // recurse through to only get the final target + $content = $this->getContent(); + $retval = $content ? $content->getUltimateRedirectTarget() : null; + if ( !$retval ) { + return null; + } + $this->insertRedirectEntry( $retval ); + return $retval; + } + + /** + * Insert or update the redirect table entry for this page to indicate + * it redirects to $rt . + * @param Title $rt Redirect target + */ + public function insertRedirectEntry( $rt ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->replace( 'redirect', array( 'rd_from' ), + array( + 'rd_from' => $this->getId(), + 'rd_namespace' => $rt->getNamespace(), + 'rd_title' => $rt->getDBkey(), + 'rd_fragment' => $rt->getFragment(), + 'rd_interwiki' => $rt->getInterwiki(), + ), + __METHOD__ + ); + } + + /** + * Get the Title object or URL this page redirects to + * + * @return bool|Title|string false, Title of in-wiki target, or string with URL + */ + public function followRedirect() { + return $this->getRedirectURL( $this->getRedirectTarget() ); + } + + /** + * Get the Title object or URL to use for a redirect. We use Title + * objects for same-wiki, non-special redirects and URLs for everything + * else. + * @param Title $rt Redirect target + * @return bool|Title|string false, Title object of local target, or string with URL + */ + public function getRedirectURL( $rt ) { + if ( !$rt ) { + return false; + } + + if ( $rt->isExternal() ) { + if ( $rt->isLocal() ) { + // Offsite wikis need an HTTP redirect. + // + // This can be hard to reverse and may produce loops, + // so they may be disabled in the site configuration. + $source = $this->mTitle->getFullURL( 'redirect=no' ); + return $rt->getFullURL( array( 'rdfrom' => $source ) ); + } else { + // External pages pages without "local" bit set are not valid + // redirect targets + return false; + } + } + + if ( $rt->isSpecialPage() ) { + // Gotta handle redirects to special pages differently: + // Fill the HTTP response "Location" header and ignore + // the rest of the page we're on. + // + // Some pages are not valid targets + if ( $rt->isValidRedirectTarget() ) { + return $rt->getFullURL(); + } else { + return false; + } + } + + return $rt; + } + + /** + * Get a list of users who have edited this article, not including the user who made + * the most recent revision, which you can get from $article->getUser() if you want it + * @return UserArrayFromResult + */ + public function getContributors() { + // @todo FIXME: This is expensive; cache this info somewhere. + + $dbr = wfGetDB( DB_SLAVE ); + + if ( $dbr->implicitGroupby() ) { + $realNameField = 'user_real_name'; + } else { + $realNameField = 'MIN(user_real_name) AS user_real_name'; + } + + $tables = array( 'revision', 'user' ); + + $fields = array( + 'user_id' => 'rev_user', + 'user_name' => 'rev_user_text', + $realNameField, + 'timestamp' => 'MAX(rev_timestamp)', + ); + + $conds = array( 'rev_page' => $this->getId() ); + + // The user who made the top revision gets credited as "this page was last edited by + // John, based on contributions by Tom, Dick and Harry", so don't include them twice. + $user = $this->getUser(); + if ( $user ) { + $conds[] = "rev_user != $user"; + } else { + $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}"; + } + + $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden? + + $jconds = array( + 'user' => array( 'LEFT JOIN', 'rev_user = user_id' ), + ); + + $options = array( + 'GROUP BY' => array( 'rev_user', 'rev_user_text' ), + 'ORDER BY' => 'timestamp DESC', + ); + + $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds ); + return new UserArrayFromResult( $res ); + } + + /** + * Get the last N authors + * @param int $num Number of revisions to get + * @param int|string $revLatest The latest rev_id, selected from the master (optional) + * @return array Array of authors, duplicates not removed + */ + public function getLastNAuthors( $num, $revLatest = 0 ) { + wfProfileIn( __METHOD__ ); + // First try the slave + // If that doesn't have the latest revision, try the master + $continue = 2; + $db = wfGetDB( DB_SLAVE ); + + do { + $res = $db->select( array( 'page', 'revision' ), + array( 'rev_id', 'rev_user_text' ), + array( + 'page_namespace' => $this->mTitle->getNamespace(), + 'page_title' => $this->mTitle->getDBkey(), + 'rev_page = page_id' + ), __METHOD__, + array( + 'ORDER BY' => 'rev_timestamp DESC', + 'LIMIT' => $num + ) + ); + + if ( !$res ) { + wfProfileOut( __METHOD__ ); + return array(); + } + + $row = $db->fetchObject( $res ); + + if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { + $db = wfGetDB( DB_MASTER ); + $continue--; + } else { + $continue = 0; + } + } while ( $continue ); + + $authors = array( $row->rev_user_text ); + + foreach ( $res as $row ) { + $authors[] = $row->rev_user_text; + } + + wfProfileOut( __METHOD__ ); + return $authors; + } + + /** + * Should the parser cache be used? + * + * @param ParserOptions $parserOptions ParserOptions to check + * @param int $oldid + * @return bool + */ + public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) { + global $wgEnableParserCache; + + return $wgEnableParserCache + && $parserOptions->getStubThreshold() == 0 + && $this->exists() + && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) + && $this->getContentHandler()->isParserCacheSupported(); + } + + /** + * Get a ParserOutput for the given ParserOptions and revision ID. + * The parser cache will be used if possible. + * + * @since 1.19 + * @param ParserOptions $parserOptions ParserOptions to use for the parse operation + * @param null|int $oldid Revision ID to get the text from, passing null or 0 will + * get the current revision (default value) + * + * @return ParserOutput|bool ParserOutput or false if the revision was not found + */ + public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { + wfProfileIn( __METHOD__ ); + + $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid ); + wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); + if ( $parserOptions->getStubThreshold() ) { + wfIncrStats( 'pcache_miss_stub' ); + } + + if ( $useParserCache ) { + $parserOutput = ParserCache::singleton()->get( $this, $parserOptions ); + if ( $parserOutput !== false ) { + wfProfileOut( __METHOD__ ); + return $parserOutput; + } + } + + if ( $oldid === null || $oldid === 0 ) { + $oldid = $this->getLatest(); + } + + $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache ); + $pool->execute(); + + wfProfileOut( __METHOD__ ); + + return $pool->getParserOutput(); + } + + /** + * Do standard deferred updates after page view (existing or missing page) + * @param User $user The relevant user + * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed. + */ + public function doViewUpdates( User $user, $oldid = 0 ) { + global $wgDisableCounters; + if ( wfReadOnly() ) { + return; + } + + // Don't update page view counters on views from bot users (bug 14044) + if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->exists() ) { + DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) ); + DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) ); + } + + // Update newtalk / watchlist notification status + $user->clearNotification( $this->mTitle, $oldid ); + } + + /** + * Perform the actions of a page purging + * @return bool + */ + public function doPurge() { + global $wgUseSquid; + + if ( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { + return false; + } + + // Invalidate the cache + $this->mTitle->invalidateCache(); + + if ( $wgUseSquid ) { + // Commit the transaction before the purge is sent + $dbw = wfGetDB( DB_MASTER ); + $dbw->commit( __METHOD__ ); + + // Send purge + $update = SquidUpdate::newSimplePurge( $this->mTitle ); + $update->doUpdate(); + } + + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + // @todo move this logic to MessageCache + + if ( $this->exists() ) { + // NOTE: use transclusion text for messages. + // This is consistent with MessageCache::getMsgFromNamespace() + + $content = $this->getContent(); + $text = $content === null ? null : $content->getWikitextForTransclusion(); + + if ( $text === null ) { + $text = false; + } + } else { + $text = false; + } + + MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text ); + } + return true; + } + + /** + * Insert a new empty page record for this article. + * This *must* be followed up by creating a revision + * and running $this->updateRevisionOn( ... ); + * or else the record will be left in a funky state. + * Best if all done inside a transaction. + * + * @param DatabaseBase $dbw + * @return int The newly created page_id key, or false if the title already existed + */ + public function insertOn( $dbw ) { + wfProfileIn( __METHOD__ ); + + $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); + $dbw->insert( 'page', array( + 'page_id' => $page_id, + 'page_namespace' => $this->mTitle->getNamespace(), + 'page_title' => $this->mTitle->getDBkey(), + 'page_counter' => 0, + 'page_restrictions' => '', + 'page_is_redirect' => 0, // Will set this shortly... + 'page_is_new' => 1, + 'page_random' => wfRandom(), + 'page_touched' => $dbw->timestamp(), + 'page_latest' => 0, // Fill this in shortly... + 'page_len' => 0, // Fill this in shortly... + ), __METHOD__, 'IGNORE' ); + + $affected = $dbw->affectedRows(); + + if ( $affected ) { + $newid = $dbw->insertId(); + $this->mId = $newid; + $this->mTitle->resetArticleID( $newid ); + } + wfProfileOut( __METHOD__ ); + + return $affected ? $newid : false; + } + + /** + * Update the page record to point to a newly saved revision. + * + * @param DatabaseBase $dbw + * @param Revision $revision For ID number, and text used to set + * length and redirect status fields + * @param int $lastRevision If given, will not overwrite the page field + * when different from the currently set value. + * Giving 0 indicates the new page flag should be set on. + * @param bool $lastRevIsRedirect If given, will optimize adding and + * removing rows in redirect table. + * @return bool true on success, false on failure + */ + public function updateRevisionOn( $dbw, $revision, $lastRevision = null, + $lastRevIsRedirect = null + ) { + global $wgContentHandlerUseDB; + + wfProfileIn( __METHOD__ ); + + $content = $revision->getContent(); + $len = $content ? $content->getSize() : 0; + $rt = $content ? $content->getUltimateRedirectTarget() : null; + + $conditions = array( 'page_id' => $this->getId() ); + + if ( !is_null( $lastRevision ) ) { + // An extra check against threads stepping on each other + $conditions['page_latest'] = $lastRevision; + } + + $now = wfTimestampNow(); + $row = array( /* SET */ + 'page_latest' => $revision->getId(), + 'page_touched' => $dbw->timestamp( $now ), + 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, + 'page_is_redirect' => $rt !== null ? 1 : 0, + 'page_len' => $len, + ); + + if ( $wgContentHandlerUseDB ) { + $row['page_content_model'] = $revision->getContentModel(); + } + + $dbw->update( 'page', + $row, + $conditions, + __METHOD__ ); + + $result = $dbw->affectedRows() > 0; + if ( $result ) { + $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); + $this->setLastEdit( $revision ); + $this->setCachedLastEditTime( $now ); + $this->mLatest = $revision->getId(); + $this->mIsRedirect = (bool)$rt; + // Update the LinkCache. + LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, + $this->mLatest, $revision->getContentModel() ); + } + + wfProfileOut( __METHOD__ ); + return $result; + } + + /** + * Add row to the redirect table if this is a redirect, remove otherwise. + * + * @param DatabaseBase $dbw + * @param Title $redirectTitle Title object pointing to the redirect target, + * or NULL if this is not a redirect + * @param null|bool $lastRevIsRedirect If given, will optimize adding and + * removing rows in redirect table. + * @return bool true on success, false on failure + * @private + */ + public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) { + // Always update redirects (target link might have changed) + // Update/Insert if we don't know if the last revision was a redirect or not + // Delete if changing from redirect to non-redirect + $isRedirect = !is_null( $redirectTitle ); + + if ( !$isRedirect && $lastRevIsRedirect === false ) { + return true; + } + + wfProfileIn( __METHOD__ ); + if ( $isRedirect ) { + $this->insertRedirectEntry( $redirectTitle ); + } else { + // This is not a redirect, remove row from redirect table + $where = array( 'rd_from' => $this->getId() ); + $dbw->delete( 'redirect', $where, __METHOD__ ); + } + + if ( $this->getTitle()->getNamespace() == NS_FILE ) { + RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); + } + wfProfileOut( __METHOD__ ); + + return ( $dbw->affectedRows() != 0 ); + } + + /** + * If the given revision is newer than the currently set page_latest, + * update the page record. Otherwise, do nothing. + * + * @deprecated since 1.24, use updateRevisionOn instead + * + * @param DatabaseBase $dbw + * @param Revision $revision + * @return bool + */ + public function updateIfNewerOn( $dbw, $revision ) { + wfProfileIn( __METHOD__ ); + + $row = $dbw->selectRow( + array( 'revision', 'page' ), + array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), + array( + 'page_id' => $this->getId(), + 'page_latest=rev_id' ), + __METHOD__ ); + + if ( $row ) { + if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) { + wfProfileOut( __METHOD__ ); + return false; + } + $prev = $row->rev_id; + $lastRevIsRedirect = (bool)$row->page_is_redirect; + } else { + // No or missing previous revision; mark the page as new + $prev = 0; + $lastRevIsRedirect = null; + } + + $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); + + wfProfileOut( __METHOD__ ); + return $ret; + } + + /** + * Get the content that needs to be saved in order to undo all revisions + * between $undo and $undoafter. Revisions must belong to the same page, + * must exist and must not be deleted + * @param Revision $undo + * @param Revision $undoafter Must be an earlier revision than $undo + * @return mixed string on success, false on failure + * @since 1.21 + * Before we had the Content object, this was done in getUndoText + */ + public function getUndoContent( Revision $undo, Revision $undoafter = null ) { + $handler = $undo->getContentHandler(); + return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter ); + } + + /** + * Get the text that needs to be saved in order to undo all revisions + * between $undo and $undoafter. Revisions must belong to the same page, + * must exist and must not be deleted + * @param Revision $undo + * @param Revision $undoafter Must be an earlier revision than $undo + * @return string|bool string on success, false on failure + * @deprecated since 1.21: use ContentHandler::getUndoContent() instead. + */ + public function getUndoText( Revision $undo, Revision $undoafter = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + $this->loadLastEdit(); + + if ( $this->mLastRevision ) { + if ( is_null( $undoafter ) ) { + $undoafter = $undo->getPrevious(); + } + + $handler = $this->getContentHandler(); + $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter ); + + if ( !$undone ) { + return false; + } else { + return ContentHandler::getContentText( $undone ); + } + } + + return false; + } + + /** + * @param string|number|null|bool $sectionId Section identifier as a number or string + * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page + * or 'new' for a new section. + * @param string $text New text of the section. + * @param string $sectionTitle New section's subject, only if $section is "new". + * @param string $edittime Revision timestamp or null to use the current revision. + * + * @throws MWException + * @return string New complete article text, or null if error. + * + * @deprecated since 1.21, use replaceSectionAtRev() instead + */ + public function replaceSection( $sectionId, $text, $sectionTitle = '', + $edittime = null + ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + //NOTE: keep condition in sync with condition in replaceSectionContent! + if ( strval( $sectionId ) === '' ) { + // Whole-page edit; let the whole text through + return $text; + } + + if ( !$this->supportsSections() ) { + throw new MWException( "sections not supported for content model " . + $this->getContentHandler()->getModelID() ); + } + + // could even make section title, but that's not required. + $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); + + $newContent = $this->replaceSectionContent( $sectionId, $sectionContent, $sectionTitle, + $edittime ); + + return ContentHandler::getContentText( $newContent ); + } + + /** + * Returns true if this page's content model supports sections. + * + * @return bool + * + * @todo The skin should check this and not offer section functionality if + * sections are not supported. + * @todo The EditPage should check this and not offer section functionality + * if sections are not supported. + */ + public function supportsSections() { + return $this->getContentHandler()->supportsSections(); + } + + /** + * @param string|number|null|bool $sectionId Section identifier as a number or string + * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page + * or 'new' for a new section. + * @param Content $sectionContent New content of the section. + * @param string $sectionTitle New section's subject, only if $section is "new". + * @param string $edittime Revision timestamp or null to use the current revision. + * + * @throws MWException + * @return Content New complete article content, or null if error. + * + * @since 1.21 + * @deprecated since 1.24, use replaceSectionAtRev instead + */ + public function replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle = '', + $edittime = null ) { + wfProfileIn( __METHOD__ ); + + $baseRevId = null; + if ( $edittime && $sectionId !== 'new' ) { + $dbw = wfGetDB( DB_MASTER ); + $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); + if ( $rev ) { + $baseRevId = $rev->getId(); + } + } + + wfProfileOut( __METHOD__ ); + return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId ); + } + + /** + * @param string|number|null|bool $sectionId Section identifier as a number or string + * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page + * or 'new' for a new section. + * @param Content $sectionContent New content of the section. + * @param string $sectionTitle New section's subject, only if $section is "new". + * @param string $baseRevId integer|null + * + * @throws MWException + * @return Content New complete article content, or null if error. + * + * @since 1.24 + */ + public function replaceSectionAtRev( $sectionId, Content $sectionContent, + $sectionTitle = '', $baseRevId = null + ) { + wfProfileIn( __METHOD__ ); + + if ( strval( $sectionId ) === '' ) { + // Whole-page edit; let the whole text through + $newContent = $sectionContent; + } else { + if ( !$this->supportsSections() ) { + wfProfileOut( __METHOD__ ); + throw new MWException( "sections not supported for content model " . + $this->getContentHandler()->getModelID() ); + } + + // Bug 30711: always use current version when adding a new section + if ( is_null( $baseRevId ) || $sectionId === 'new' ) { + $oldContent = $this->getContent(); + } else { + // TODO: try DB_SLAVE first + $dbw = wfGetDB( DB_MASTER ); + $rev = Revision::loadFromId( $dbw, $baseRevId ); + + if ( !$rev ) { + wfDebug( __METHOD__ . " asked for bogus section (page: " . + $this->getId() . "; section: $sectionId)\n" ); + wfProfileOut( __METHOD__ ); + return null; + } + + $oldContent = $rev->getContent(); + } + + if ( ! $oldContent ) { + wfDebug( __METHOD__ . ": no page text\n" ); + wfProfileOut( __METHOD__ ); + return null; + } + + $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle ); + } + + wfProfileOut( __METHOD__ ); + return $newContent; + } + + /** + * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed. + * @param int $flags + * @return int Updated $flags + */ + public function checkFlags( $flags ) { + if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) { + if ( $this->exists() ) { + $flags |= EDIT_UPDATE; + } else { + $flags |= EDIT_NEW; + } + } + + return $flags; + } + + /** + * Change an existing article or create a new article. Updates RC and all necessary caches, + * optionally via the deferred update array. + * + * @param string $text New text + * @param string $summary Edit summary + * @param int $flags Bitfield: + * EDIT_NEW + * Article is known or assumed to be non-existent, create a new one + * EDIT_UPDATE + * Article is known or assumed to be pre-existing, update it + * EDIT_MINOR + * Mark this edit minor, if the user is allowed to do so + * EDIT_SUPPRESS_RC + * Do not log the change in recentchanges + * EDIT_FORCE_BOT + * Mark the edit a "bot" edit regardless of user rights + * EDIT_DEFER_UPDATES + * Defer some of the updates until the end of index.php + * EDIT_AUTOSUMMARY + * Fill in blank summaries with generated text where possible + * + * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the + * article will be detected. If EDIT_UPDATE is specified and the article + * doesn't exist, the function will return an edit-gone-missing error. If + * EDIT_NEW is specified and the article does exist, an edit-already-exists + * error will be returned. These two conditions are also possible with + * auto-detection due to MediaWiki's performance-optimised locking strategy. + * + * @param bool|int $baseRevId The revision ID this edit was based off, if any + * @param User $user The user doing the edit + * + * @throws MWException + * @return Status object. Possible errors: + * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't + * set the fatal flag of $status + * edit-gone-missing: In update mode, but the article didn't exist. + * edit-conflict: In update mode, the article changed unexpectedly. + * edit-no-change: Warning that the text was the same as before. + * edit-already-exists: In creation mode, but the article already exists. + * + * Extensions may define additional errors. + * + * $return->value will contain an associative array with members as follows: + * new: Boolean indicating if the function attempted to create a new article. + * revision: The revision object for the inserted revision, or null. + * + * Compatibility note: this function previously returned a boolean value + * indicating success/failure + * + * @deprecated since 1.21: use doEditContent() instead. + */ + public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + + return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user ); + } + + /** + * Change an existing article or create a new article. Updates RC and all necessary caches, + * optionally via the deferred update array. + * + * @param Content $content New content + * @param string $summary Edit summary + * @param int $flags Bitfield: + * EDIT_NEW + * Article is known or assumed to be non-existent, create a new one + * EDIT_UPDATE + * Article is known or assumed to be pre-existing, update it + * EDIT_MINOR + * Mark this edit minor, if the user is allowed to do so + * EDIT_SUPPRESS_RC + * Do not log the change in recentchanges + * EDIT_FORCE_BOT + * Mark the edit a "bot" edit regardless of user rights + * EDIT_DEFER_UPDATES + * Defer some of the updates until the end of index.php + * EDIT_AUTOSUMMARY + * Fill in blank summaries with generated text where possible + * + * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the + * article will be detected. If EDIT_UPDATE is specified and the article + * doesn't exist, the function will return an edit-gone-missing error. If + * EDIT_NEW is specified and the article does exist, an edit-already-exists + * error will be returned. These two conditions are also possible with + * auto-detection due to MediaWiki's performance-optimised locking strategy. + * + * @param bool|int $baseRevId The revision ID this edit was based off, if any + * @param User $user The user doing the edit + * @param string $serialisation_format Format for storing the content in the + * database. + * + * @throws MWException + * @return Status object. Possible errors: + * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't + * set the fatal flag of $status. + * edit-gone-missing: In update mode, but the article didn't exist. + * edit-conflict: In update mode, the article changed unexpectedly. + * edit-no-change: Warning that the text was the same as before. + * edit-already-exists: In creation mode, but the article already exists. + * + * Extensions may define additional errors. + * + * $return->value will contain an associative array with members as follows: + * new: Boolean indicating if the function attempted to create a new article. + * revision: The revision object for the inserted revision, or null. + * + * @since 1.21 + */ + public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, + User $user = null, $serialisation_format = null + ) { + global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; + + // Low-level sanity check + if ( $this->mTitle->getText() === '' ) { + throw new MWException( 'Something is trying to edit an article with an empty title' ); + } + + wfProfileIn( __METHOD__ ); + + if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) { + wfProfileOut( __METHOD__ ); + return Status::newFatal( 'content-not-allowed-here', + ContentHandler::getLocalizedName( $content->getModel() ), + $this->getTitle()->getPrefixedText() ); + } + + $user = is_null( $user ) ? $wgUser : $user; + $status = Status::newGood( array() ); + + // Load the data from the master database if needed. + // The caller may already loaded it from the master or even loaded it using + // SELECT FOR UPDATE, so do not override that using clear(). + $this->loadPageData( 'fromdbmaster' ); + + $flags = $this->checkFlags( $flags ); + + // handle hook + $hook_args = array( &$this, &$user, &$content, &$summary, + $flags & EDIT_MINOR, null, null, &$flags, &$status ); + + if ( !wfRunHooks( 'PageContentSave', $hook_args ) + || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) { + + wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); + + if ( $status->isOK() ) { + $status->fatal( 'edit-hook-aborted' ); + } + + wfProfileOut( __METHOD__ ); + return $status; + } + + // Silently ignore EDIT_MINOR if not allowed + $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); + $bot = $flags & EDIT_FORCE_BOT; + + $old_content = $this->getContent( Revision::RAW ); // current revision's content + + $oldsize = $old_content ? $old_content->getSize() : 0; + $oldid = $this->getLatest(); + $oldIsRedirect = $this->isRedirect(); + $oldcountable = $this->isCountable(); + + $handler = $content->getContentHandler(); + + // Provide autosummaries if one is not provided and autosummaries are enabled. + if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { + if ( !$old_content ) { + $old_content = null; + } + $summary = $handler->getAutosummary( $old_content, $content, $flags ); + } + + $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); + $serialized = $editInfo->pst; + + /** + * @var Content $content + */ + $content = $editInfo->pstContent; + $newsize = $content->getSize(); + + $dbw = wfGetDB( DB_MASTER ); + $now = wfTimestampNow(); + $this->mTimestamp = $now; + + if ( $flags & EDIT_UPDATE ) { + // Update article, but only if changed. + $status->value['new'] = false; + + if ( !$oldid ) { + // Article gone missing + wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); + $status->fatal( 'edit-gone-missing' ); + + wfProfileOut( __METHOD__ ); + return $status; + } elseif ( !$old_content ) { + // Sanity check for bug 37225 + wfProfileOut( __METHOD__ ); + throw new MWException( "Could not find text for current revision {$oldid}." ); + } + + $revision = new Revision( array( + 'page' => $this->getId(), + 'title' => $this->getTitle(), // for determining the default content model + 'comment' => $summary, + 'minor_edit' => $isminor, + 'text' => $serialized, + 'len' => $newsize, + 'parent_id' => $oldid, + 'user' => $user->getId(), + 'user_text' => $user->getName(), + 'timestamp' => $now, + 'content_model' => $content->getModel(), + 'content_format' => $serialisation_format, + ) ); // XXX: pass content object?! + + $changed = !$content->equals( $old_content ); + + if ( $changed ) { + if ( !$content->isValid() ) { + wfProfileOut( __METHOD__ ); + throw new MWException( "New content failed validity check!" ); + } + + $dbw->begin( __METHOD__ ); + try { + + $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); + $status->merge( $prepStatus ); + + if ( !$status->isOK() ) { + $dbw->rollback( __METHOD__ ); + + wfProfileOut( __METHOD__ ); + return $status; + } + $revisionId = $revision->insertOn( $dbw ); + + // Update page + // + // We check for conflicts by comparing $oldid with the current latest revision ID. + $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); + + if ( !$ok ) { + // Belated edit conflict! Run away!! + $status->fatal( 'edit-conflict' ); + + $dbw->rollback( __METHOD__ ); + + wfProfileOut( __METHOD__ ); + return $status; + } + + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); + // Update recentchanges + if ( !( $flags & EDIT_SUPPRESS_RC ) ) { + // Mark as patrolled if the user can do so + $patrolled = $wgUseRCPatrol && !count( + $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); + // Add RC row to the DB + $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, + $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, + $revisionId, $patrolled + ); + + // Log auto-patrolled edits + if ( $patrolled ) { + PatrolLog::record( $rc, true, $user ); + } + } + $user->incEditCount(); + } catch ( MWException $e ) { + $dbw->rollback( __METHOD__ ); + // Question: Would it perhaps be better if this method turned all + // exceptions into $status's? + throw $e; + } + $dbw->commit( __METHOD__ ); + } else { + // Bug 32948: revision ID must be set to page {{REVISIONID}} and + // related variables correctly + $revision->setId( $this->getLatest() ); + } + + // Update links tables, site stats, etc. + $this->doEditUpdates( + $revision, + $user, + array( + 'changed' => $changed, + 'oldcountable' => $oldcountable + ) + ); + + if ( !$changed ) { + $status->warning( 'edit-no-change' ); + $revision = null; + // Update page_touched, this is usually implicit in the page update + // Other cache updates are done in onArticleEdit() + $this->mTitle->invalidateCache(); + } + } else { + // Create new article + $status->value['new'] = true; + + $dbw->begin( __METHOD__ ); + try { + + $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); + $status->merge( $prepStatus ); + + if ( !$status->isOK() ) { + $dbw->rollback( __METHOD__ ); + + wfProfileOut( __METHOD__ ); + return $status; + } + + $status->merge( $prepStatus ); + + // Add the page record; stake our claim on this title! + // This will return false if the article already exists + $newid = $this->insertOn( $dbw ); + + if ( $newid === false ) { + $dbw->rollback( __METHOD__ ); + $status->fatal( 'edit-already-exists' ); + + wfProfileOut( __METHOD__ ); + return $status; + } + + // Save the revision text... + $revision = new Revision( array( + 'page' => $newid, + 'title' => $this->getTitle(), // for determining the default content model + 'comment' => $summary, + 'minor_edit' => $isminor, + 'text' => $serialized, + 'len' => $newsize, + 'user' => $user->getId(), + 'user_text' => $user->getName(), + 'timestamp' => $now, + 'content_model' => $content->getModel(), + 'content_format' => $serialisation_format, + ) ); + $revisionId = $revision->insertOn( $dbw ); + + // Bug 37225: use accessor to get the text as Revision may trim it + $content = $revision->getContent(); // sanity; get normalized version + + if ( $content ) { + $newsize = $content->getSize(); + } + + // Update the page record with revision data + $this->updateRevisionOn( $dbw, $revision, 0 ); + + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); + + // Update recentchanges + if ( !( $flags & EDIT_SUPPRESS_RC ) ) { + // Mark as patrolled if the user can do so + $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( + $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); + // Add RC row to the DB + $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, + '', $newsize, $revisionId, $patrolled ); + + // Log auto-patrolled edits + if ( $patrolled ) { + PatrolLog::record( $rc, true, $user ); + } + } + $user->incEditCount(); + + } catch ( MWException $e ) { + $dbw->rollback( __METHOD__ ); + throw $e; + } + $dbw->commit( __METHOD__ ); + + // Update links, etc. + $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); + + $hook_args = array( &$this, &$user, $content, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision ); + + ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args ); + wfRunHooks( 'PageContentInsertComplete', $hook_args ); + } + + // Do updates right now unless deferral was requested + if ( !( $flags & EDIT_DEFER_UPDATES ) ) { + DeferredUpdates::doUpdates(); + } + + // Return the new revision (or null) to the caller + $status->value['revision'] = $revision; + + $hook_args = array( &$this, &$user, $content, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); + + ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); + wfRunHooks( 'PageContentSaveComplete', $hook_args ); + + // Promote user to any groups they meet the criteria for + $user->addAutopromoteOnceGroups( 'onEdit' ); + + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * Get parser options suitable for rendering the primary article wikitext + * + * @see ContentHandler::makeParserOptions + * + * @param IContextSource|User|string $context One of the following: + * - IContextSource: Use the User and the Language of the provided + * context + * - User: Use the provided User object and $wgLang for the language, + * so use an IContextSource object if possible. + * - 'canonical': Canonical options (anonymous user with default + * preferences and content language). + * @return ParserOptions + */ + public function makeParserOptions( $context ) { + $options = $this->getContentHandler()->makeParserOptions( $context ); + + if ( $this->getTitle()->isConversionTable() ) { + // @todo ConversionTable should become a separate content model, so + // we don't need special cases like this one. + $options->disableContentConversion(); + } + + return $options; + } + + /** + * Prepare text which is about to be saved. + * Returns a stdclass with source, pst and output members + * + * @deprecated since 1.21: use prepareContentForEdit instead. + * @return object + */ + public function prepareTextForEdit( $text, $revid = null, User $user = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + return $this->prepareContentForEdit( $content, $revid, $user ); + } + + /** + * Prepare content which is about to be saved. + * Returns a stdclass with source, pst and output members + * + * @param Content $content + * @param int|null $revid + * @param User|null $user + * @param string|null $serialization_format + * + * @return bool|object + * + * @since 1.21 + */ + public function prepareContentForEdit( Content $content, $revid = null, User $user = null, + $serialization_format = null + ) { + global $wgContLang, $wgUser; + $user = is_null( $user ) ? $wgUser : $user; + //XXX: check $user->getId() here??? + + // Use a sane default for $serialization_format, see bug 57026 + if ( $serialization_format === null ) { + $serialization_format = $content->getContentHandler()->getDefaultFormat(); + } + + if ( $this->mPreparedEdit + && $this->mPreparedEdit->newContent + && $this->mPreparedEdit->newContent->equals( $content ) + && $this->mPreparedEdit->revid == $revid + && $this->mPreparedEdit->format == $serialization_format + // XXX: also check $user here? + ) { + // Already prepared + return $this->mPreparedEdit; + } + + $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); + wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) ); + + $edit = (object)array(); + $edit->revid = $revid; + $edit->timestamp = wfTimestampNow(); + + $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null; + + $edit->format = $serialization_format; + $edit->popts = $this->makeParserOptions( 'canonical' ); + $edit->output = $edit->pstContent + ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ) + : null; + + $edit->newContent = $content; + $edit->oldContent = $this->getContent( Revision::RAW ); + + // NOTE: B/C for hooks! don't use these fields! + $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : ''; + $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; + $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : ''; + + $this->mPreparedEdit = $edit; + return $edit; + } + + /** + * Do standard deferred updates after page edit. + * Update links tables, site stats, search index and message cache. + * Purges pages that include this page if the text was changed here. + * Every 100th edit, prune the recent changes table. + * + * @param Revision $revision + * @param User $user User object that did the revision + * @param array $options Array of options, following indexes are used: + * - changed: boolean, whether the revision changed the content (default true) + * - created: boolean, whether the revision created the page (default false) + * - oldcountable: boolean or null (default null): + * - boolean: whether the page was counted as an article before that + * revision, only used in changed is true and created is false + * - null: don't change the article count + */ + public function doEditUpdates( Revision $revision, User $user, array $options = array() ) { + global $wgEnableParserCache; + + wfProfileIn( __METHOD__ ); + + $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); + $content = $revision->getContent(); + + // Parse the text + // Be careful not to do pre-save transform twice: $text is usually + // already pre-save transformed once. + if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { + wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); + $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user ); + } else { + wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); + $editInfo = $this->mPreparedEdit; + } + + // Save it to the parser cache + if ( $wgEnableParserCache ) { + $parserCache = ParserCache::singleton(); + $parserCache->save( + $editInfo->output, $this, $editInfo->popts, $editInfo->timestamp, $editInfo->revid + ); + } + + // Update the links tables and other secondary data + if ( $content ) { + $recursive = $options['changed']; // bug 50785 + $updates = $content->getSecondaryDataUpdates( + $this->getTitle(), null, $recursive, $editInfo->output ); + DataUpdate::runUpdates( $updates ); + } + + wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); + + if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { + if ( 0 == mt_rand( 0, 99 ) ) { + // Flush old entries from the `recentchanges` table; we do this on + // random requests so as to avoid an increase in writes for no good reason + RecentChange::purgeExpiredChanges(); + } + } + + if ( !$this->exists() ) { + wfProfileOut( __METHOD__ ); + return; + } + + $id = $this->getId(); + $title = $this->mTitle->getPrefixedDBkey(); + $shortTitle = $this->mTitle->getDBkey(); + + if ( !$options['changed'] ) { + $good = 0; + } elseif ( $options['created'] ) { + $good = (int)$this->isCountable( $editInfo ); + } elseif ( $options['oldcountable'] !== null ) { + $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable']; + } else { + $good = 0; + } + $edits = $options['changed'] ? 1 : 0; + $total = $options['created'] ? 1 : 0; + + DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) ); + DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) ); + + // If this is another user's talk page, update newtalk. + // Don't do this if $options['changed'] = false (null-edits) nor if + // it's a minor edit and the user doesn't want notifications for those. + if ( $options['changed'] + && $this->mTitle->getNamespace() == NS_USER_TALK + && $shortTitle != $user->getTitleKey() + && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) ) + ) { + $recipient = User::newFromName( $shortTitle, false ); + if ( !$recipient ) { + wfDebug( __METHOD__ . ": invalid username\n" ); + } else { + // Allow extensions to prevent user notification when a new message is added to their talk page + if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) { + if ( User::isIP( $shortTitle ) ) { + // An anonymous user + $recipient->setNewtalk( true, $revision ); + } elseif ( $recipient->isLoggedIn() ) { + $recipient->setNewtalk( true, $revision ); + } else { + wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); + } + } + } + } + + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + // XXX: could skip pseudo-messages like js/css here, based on content model. + $msgtext = $content ? $content->getWikitextForTransclusion() : null; + if ( $msgtext === false || $msgtext === null ) { + $msgtext = ''; + } + + MessageCache::singleton()->replace( $shortTitle, $msgtext ); + } + + if ( $options['created'] ) { + self::onArticleCreate( $this->mTitle ); + } elseif ( $options['changed'] ) { // bug 50785 + self::onArticleEdit( $this->mTitle ); + } + + wfProfileOut( __METHOD__ ); + } + + /** + * Edit an article without doing all that other stuff + * The article must already exist; link tables etc + * are not updated, caches are not flushed. + * + * @param string $text Text submitted + * @param User $user The relevant user + * @param string $comment Comment submitted + * @param bool $minor Whereas it's a minor modification + * + * @deprecated since 1.21, use doEditContent() instead. + */ + public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { + ContentHandler::deprecated( __METHOD__, "1.21" ); + + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + $this->doQuickEditContent( $content, $user, $comment, $minor ); + } + + /** + * Edit an article without doing all that other stuff + * The article must already exist; link tables etc + * are not updated, caches are not flushed. + * + * @param Content $content Content submitted + * @param User $user The relevant user + * @param string $comment comment submitted + * @param string $serialisation_format Format for storing the content in the database + * @param bool $minor Whereas it's a minor modification + */ + public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = false, + $serialisation_format = null + ) { + wfProfileIn( __METHOD__ ); + + $serialized = $content->serialize( $serialisation_format ); + + $dbw = wfGetDB( DB_MASTER ); + $revision = new Revision( array( + 'title' => $this->getTitle(), // for determining the default content model + 'page' => $this->getId(), + 'user_text' => $user->getName(), + 'user' => $user->getId(), + 'text' => $serialized, + 'length' => $content->getSize(), + 'comment' => $comment, + 'minor_edit' => $minor ? 1 : 0, + ) ); // XXX: set the content object? + $revision->insertOn( $dbw ); + $this->updateRevisionOn( $dbw, $revision ); + + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); + + wfProfileOut( __METHOD__ ); + } + + /** + * Update the article's restriction field, and leave a log entry. + * This works for protection both existing and non-existing pages. + * + * @param array $limit Set of restriction keys + * @param array $expiry Per restriction type expiration + * @param int &$cascade Set to false if cascading protection isn't allowed. + * @param string $reason + * @param User $user The user updating the restrictions + * @return Status + */ + public function doUpdateRestrictions( array $limit, array $expiry, + &$cascade, $reason, User $user + ) { + global $wgCascadingRestrictionLevels, $wgContLang; + + if ( wfReadOnly() ) { + return Status::newFatal( 'readonlytext', wfReadOnlyReason() ); + } + + $this->loadPageData( 'fromdbmaster' ); + $restrictionTypes = $this->mTitle->getRestrictionTypes(); + $id = $this->getId(); + + if ( !$cascade ) { + $cascade = false; + } + + // Take this opportunity to purge out expired restrictions + Title::purgeExpiredRestrictions(); + + // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); + // we expect a single selection, but the schema allows otherwise. + $isProtected = false; + $protect = false; + $changed = false; + + $dbw = wfGetDB( DB_MASTER ); + + foreach ( $restrictionTypes as $action ) { + if ( !isset( $expiry[$action] ) ) { + $expiry[$action] = $dbw->getInfinity(); + } + if ( !isset( $limit[$action] ) ) { + $limit[$action] = ''; + } elseif ( $limit[$action] != '' ) { + $protect = true; + } + + // Get current restrictions on $action + $current = implode( '', $this->mTitle->getRestrictions( $action ) ); + if ( $current != '' ) { + $isProtected = true; + } + + if ( $limit[$action] != $current ) { + $changed = true; + } elseif ( $limit[$action] != '' ) { + // Only check expiry change if the action is actually being + // protected, since expiry does nothing on an not-protected + // action. + if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) { + $changed = true; + } + } + } + + if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) { + $changed = true; + } + + // If nothing has changed, do nothing + if ( !$changed ) { + return Status::newGood(); + } + + if ( !$protect ) { // No protection at all means unprotection + $revCommentMsg = 'unprotectedarticle'; + $logAction = 'unprotect'; + } elseif ( $isProtected ) { + $revCommentMsg = 'modifiedarticleprotection'; + $logAction = 'modify'; + } else { + $revCommentMsg = 'protectedarticle'; + $logAction = 'protect'; + } + + // Truncate for whole multibyte characters + $reason = $wgContLang->truncate( $reason, 255 ); + + $logRelationsValues = array(); + $logRelationsField = null; + + if ( $id ) { // Protection of existing page + if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) { + return Status::newGood(); + } + + // Only certain restrictions can cascade... + $editrestriction = isset( $limit['edit'] ) + ? array( $limit['edit'] ) + : $this->mTitle->getRestrictions( 'edit' ); + foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) { + $editrestriction[$key] = 'editprotected'; // backwards compatibility + } + foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) { + $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility + } + + $cascadingRestrictionLevels = $wgCascadingRestrictionLevels; + foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) { + $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility + } + foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) { + $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility + } + + // The schema allows multiple restrictions + if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) { + $cascade = false; + } + + // insert null revision to identify the page protection change as edit summary + $latest = $this->getLatest(); + $nullRevision = $this->insertProtectNullRevision( + $revCommentMsg, + $limit, + $expiry, + $cascade, + $reason, + $user + ); + + if ( $nullRevision === null ) { + return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() ); + } + + $logRelationsField = 'pr_id'; + + // Update restrictions table + foreach ( $limit as $action => $restrictions ) { + $dbw->delete( + 'page_restrictions', + array( + 'pr_page' => $id, + 'pr_type' => $action + ), + __METHOD__ + ); + if ( $restrictions != '' ) { + $dbw->insert( + 'page_restrictions', + array( + 'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ), + 'pr_page' => $id, + 'pr_type' => $action, + 'pr_level' => $restrictions, + 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0, + 'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] ) + ), + __METHOD__ + ); + $logRelationsValues[] = $dbw->insertId(); + } + } + + // Clear out legacy restriction fields + $dbw->update( + 'page', + array( 'page_restrictions' => '' ), + array( 'page_id' => $id ), + __METHOD__ + ); + + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) ); + wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) ); + } else { // Protection of non-existing page (also known as "title protection") + // Cascade protection is meaningless in this case + $cascade = false; + + if ( $limit['create'] != '' ) { + $dbw->replace( 'protected_titles', + array( array( 'pt_namespace', 'pt_title' ) ), + array( + 'pt_namespace' => $this->mTitle->getNamespace(), + 'pt_title' => $this->mTitle->getDBkey(), + 'pt_create_perm' => $limit['create'], + 'pt_timestamp' => $dbw->timestamp(), + 'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ), + 'pt_user' => $user->getId(), + 'pt_reason' => $reason, + ), __METHOD__ + ); + } else { + $dbw->delete( 'protected_titles', + array( + 'pt_namespace' => $this->mTitle->getNamespace(), + 'pt_title' => $this->mTitle->getDBkey() + ), __METHOD__ + ); + } + } + + $this->mTitle->flushRestrictions(); + InfoAction::invalidateCache( $this->mTitle ); + + if ( $logAction == 'unprotect' ) { + $params = array(); + } else { + $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry ); + $params = array( $protectDescriptionLog, $cascade ? 'cascade' : '' ); + } + + // Update the protection log + $log = new LogPage( 'protect' ); + $logId = $log->addEntry( $logAction, $this->mTitle, $reason, $params, $user ); + if ( $logRelationsField !== null && count( $logRelationsValues ) ) { + $log->addRelations( $logRelationsField, $logRelationsValues, $logId ); + } + + return Status::newGood(); + } + + /** + * Insert a new null revision for this page. + * + * @param string $revCommentMsg Comment message key for the revision + * @param array $limit Set of restriction keys + * @param array $expiry Per restriction type expiration + * @param int $cascade Set to false if cascading protection isn't allowed. + * @param string $reason + * @param User|null $user + * @return Revision|null Null on error + */ + public function insertProtectNullRevision( $revCommentMsg, array $limit, + array $expiry, $cascade, $reason, $user = null + ) { + global $wgContLang; + $dbw = wfGetDB( DB_MASTER ); + + // Prepare a null revision to be added to the history + $editComment = $wgContLang->ucfirst( + wfMessage( + $revCommentMsg, + $this->mTitle->getPrefixedText() + )->inContentLanguage()->text() + ); + if ( $reason ) { + $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; + } + $protectDescription = $this->protectDescription( $limit, $expiry ); + if ( $protectDescription ) { + $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); + $editComment .= wfMessage( 'parentheses' )->params( $protectDescription ) + ->inContentLanguage()->text(); + } + if ( $cascade ) { + $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); + $editComment .= wfMessage( 'brackets' )->params( + wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text() + )->inContentLanguage()->text(); + } + + $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user ); + if ( $nullRev ) { + $nullRev->insertOn( $dbw ); + + // Update page record and touch page + $oldLatest = $nullRev->getParentId(); + $this->updateRevisionOn( $dbw, $nullRev, $oldLatest ); + } + + return $nullRev; + } + + /** + * @param string $expiry 14-char timestamp or "infinity", or false if the input was invalid + * @return string + */ + protected function formatExpiry( $expiry ) { + global $wgContLang; + $dbr = wfGetDB( DB_SLAVE ); + + $encodedExpiry = $dbr->encodeExpiry( $expiry ); + if ( $encodedExpiry != 'infinity' ) { + return wfMessage( + 'protect-expiring', + $wgContLang->timeanddate( $expiry, false, false ), + $wgContLang->date( $expiry, false, false ), + $wgContLang->time( $expiry, false, false ) + )->inContentLanguage()->text(); + } else { + return wfMessage( 'protect-expiry-indefinite' ) + ->inContentLanguage()->text(); + } + } + + /** + * Builds the description to serve as comment for the edit. + * + * @param array $limit Set of restriction keys + * @param array $expiry Per restriction type expiration + * @return string + */ + public function protectDescription( array $limit, array $expiry ) { + $protectDescription = ''; + + foreach ( array_filter( $limit ) as $action => $restrictions ) { + # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ). + # All possible message keys are listed here for easier grepping: + # * restriction-create + # * restriction-edit + # * restriction-move + # * restriction-upload + $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text(); + # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ), + # with '' filtered out. All possible message keys are listed below: + # * protect-level-autoconfirmed + # * protect-level-sysop + $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text(); + + $expiryText = $this->formatExpiry( $expiry[$action] ); + + if ( $protectDescription !== '' ) { + $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text(); + } + $protectDescription .= wfMessage( 'protect-summary-desc' ) + ->params( $actionText, $restrictionsText, $expiryText ) + ->inContentLanguage()->text(); + } + + return $protectDescription; + } + + /** + * Builds the description to serve as comment for the log entry. + * + * Some bots may parse IRC lines, which are generated from log entries which contain plain + * protect description text. Keep them in old format to avoid breaking compatibility. + * TODO: Fix protection log to store structured description and format it on-the-fly. + * + * @param array $limit Set of restriction keys + * @param array $expiry Per restriction type expiration + * @return string + */ + public function protectDescriptionLog( array $limit, array $expiry ) { + global $wgContLang; + + $protectDescriptionLog = ''; + + foreach ( array_filter( $limit ) as $action => $restrictions ) { + $expiryText = $this->formatExpiry( $expiry[$action] ); + $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ($expiryText)"; + } + + return trim( $protectDescriptionLog ); + } + + /** + * Take an array of page restrictions and flatten it to a string + * suitable for insertion into the page_restrictions field. + * + * @param string[] $limit + * + * @throws MWException + * @return string + */ + protected static function flattenRestrictions( $limit ) { + if ( !is_array( $limit ) ) { + throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' ); + } + + $bits = array(); + ksort( $limit ); + + foreach ( array_filter( $limit ) as $action => $restrictions ) { + $bits[] = "$action=$restrictions"; + } + + return implode( ':', $bits ); + } + + /** + * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for + * backwards compatibility, if you care about error reporting you should use + * doDeleteArticleReal() instead. + * + * Deletes the article with database consistency, writes logs, purges caches + * + * @param string $reason Delete reason for deletion log + * @param bool $suppress Suppress all revisions and log the deletion in + * the suppression log instead of the deletion log + * @param int $id Article ID + * @param bool $commit Defaults to true, triggers transaction end + * @param array &$error Array of errors to append to + * @param User $user The deleting user + * @return bool true if successful + */ + public function doDeleteArticle( + $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null + ) { + $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user ); + return $status->isGood(); + } + + /** + * Back-end article deletion + * Deletes the article with database consistency, writes logs, purges caches + * + * @since 1.19 + * + * @param string $reason Delete reason for deletion log + * @param bool $suppress Suppress all revisions and log the deletion in + * the suppression log instead of the deletion log + * @param int $id Article ID + * @param bool $commit Defaults to true, triggers transaction end + * @param array &$error Array of errors to append to + * @param User $user The deleting user + * @return Status Status object; if successful, $status->value is the log_id of the + * deletion log entry. If the page couldn't be deleted because it wasn't + * found, $status is a non-fatal 'cannotdelete' error + */ + public function doDeleteArticleReal( + $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null + ) { + global $wgUser, $wgContentHandlerUseDB; + + wfDebug( __METHOD__ . "\n" ); + + $status = Status::newGood(); + + if ( $this->mTitle->getDBkey() === '' ) { + $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); + return $status; + } + + $user = is_null( $user ) ? $wgUser : $user; + if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) { + if ( $status->isOK() ) { + // Hook aborted but didn't set a fatal status + $status->fatal( 'delete-hook-aborted' ); + } + return $status; + } + + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin( __METHOD__ ); + + if ( $id == 0 ) { + $this->loadPageData( 'forupdate' ); + $id = $this->getID(); + if ( $id == 0 ) { + $dbw->rollback( __METHOD__ ); + $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); + return $status; + } + } + + // we need to remember the old content so we can use it to generate all deletion updates. + $content = $this->getContent( Revision::RAW ); + + // Bitfields to further suppress the content + if ( $suppress ) { + $bitfield = 0; + // This should be 15... + $bitfield |= Revision::DELETED_TEXT; + $bitfield |= Revision::DELETED_COMMENT; + $bitfield |= Revision::DELETED_USER; + $bitfield |= Revision::DELETED_RESTRICTED; + } else { + $bitfield = 'rev_deleted'; + } + + // For now, shunt the revision data into the archive table. + // Text is *not* removed from the text table; bulk storage + // is left intact to avoid breaking block-compression or + // immutable storage schemes. + // + // For backwards compatibility, note that some older archive + // table entries will have ar_text and ar_flags fields still. + // + // In the future, we may keep revisions and mark them with + // the rev_deleted field, which is reserved for this purpose. + + $row = array( + 'ar_namespace' => 'page_namespace', + 'ar_title' => 'page_title', + 'ar_comment' => 'rev_comment', + 'ar_user' => 'rev_user', + 'ar_user_text' => 'rev_user_text', + 'ar_timestamp' => 'rev_timestamp', + 'ar_minor_edit' => 'rev_minor_edit', + 'ar_rev_id' => 'rev_id', + 'ar_parent_id' => 'rev_parent_id', + 'ar_text_id' => 'rev_text_id', + 'ar_text' => '\'\'', // Be explicit to appease + 'ar_flags' => '\'\'', // MySQL's "strict mode"... + 'ar_len' => 'rev_len', + 'ar_page_id' => 'page_id', + 'ar_deleted' => $bitfield, + 'ar_sha1' => 'rev_sha1', + ); + + if ( $wgContentHandlerUseDB ) { + $row['ar_content_model'] = 'rev_content_model'; + $row['ar_content_format'] = 'rev_content_format'; + } + + $dbw->insertSelect( 'archive', array( 'page', 'revision' ), + $row, + array( + 'page_id' => $id, + 'page_id = rev_page' + ), __METHOD__ + ); + + // Now that it's safely backed up, delete it + $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); + $ok = ( $dbw->affectedRows() > 0 ); // $id could be laggy + + if ( !$ok ) { + $dbw->rollback( __METHOD__ ); + $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); + return $status; + } + + if ( !$dbw->cascadingDeletes() ) { + $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); + } + + // Clone the title, so we have the information we need when we log + $logTitle = clone $this->mTitle; + + $this->doDeleteUpdates( $id, $content ); + + // Log the deletion, if the page was suppressed, log it at Oversight instead + $logtype = $suppress ? 'suppress' : 'delete'; + + $logEntry = new ManualLogEntry( $logtype, 'delete' ); + $logEntry->setPerformer( $user ); + $logEntry->setTarget( $logTitle ); + $logEntry->setComment( $reason ); + $logid = $logEntry->insert(); + + $dbw->onTransactionPreCommitOrIdle( function() use ( $dbw, $logEntry, $logid ) { + // Bug 56776: avoid deadlocks (especially from FileDeleteForm) + $logEntry->publish( $logid ); + } ); + + if ( $commit ) { + $dbw->commit( __METHOD__ ); + } + + wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) ); + $status->value = $logid; + return $status; + } + + /** + * Do some database updates after deletion + * + * @param int $id page_id value of the page being deleted + * @param Content $content Optional page content to be used when determining + * the required updates. This may be needed because $this->getContent() + * may already return null when the page proper was deleted. + */ + public function doDeleteUpdates( $id, Content $content = null ) { + // update site status + DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); + + // remove secondary indexes, etc + $updates = $this->getDeletionUpdates( $content ); + DataUpdate::runUpdates( $updates ); + + // Reparse any pages transcluding this page + LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' ); + + // Reparse any pages including this image + if ( $this->mTitle->getNamespace() == NS_FILE ) { + LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' ); + } + + // Clear caches + WikiPage::onArticleDelete( $this->mTitle ); + + // Reset this object and the Title object + $this->loadFromRow( false, self::READ_LATEST ); + + // Search engine + DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) ); + } + + /** + * Roll back the most recent consecutive set of edits to a page + * from the same user; fails if there are no eligible edits to + * roll back to, e.g. user is the sole contributor. This function + * performs permissions checks on $user, then calls commitRollback() + * to do the dirty work + * + * @todo Separate the business/permission stuff out from backend code + * + * @param string $fromP Name of the user whose edits to rollback. + * @param string $summary Custom summary. Set to default summary if empty. + * @param string $token Rollback token. + * @param bool $bot If true, mark all reverted edits as bot. + * + * @param array $resultDetails contains result-specific array of additional values + * 'alreadyrolled' : 'current' (rev) + * success : 'summary' (str), 'current' (rev), 'target' (rev) + * + * @param User $user The user performing the rollback + * @return array Array of errors, each error formatted as + * array(messagekey, param1, param2, ...). + * On success, the array is empty. This array can also be passed to + * OutputPage::showPermissionsErrorPage(). + */ + public function doRollback( + $fromP, $summary, $token, $bot, &$resultDetails, User $user + ) { + $resultDetails = null; + + // Check permissions + $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user ); + $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user ); + $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); + + if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) { + $errors[] = array( 'sessionfailure' ); + } + + if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) { + $errors[] = array( 'actionthrottledtext' ); + } + + // If there were errors, bail out now + if ( !empty( $errors ) ) { + return $errors; + } + + return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user ); + } + + /** + * Backend implementation of doRollback(), please refer there for parameter + * and return value documentation + * + * NOTE: This function does NOT check ANY permissions, it just commits the + * rollback to the DB. Therefore, you should only call this function direct- + * ly if you want to use custom permissions checks. If you don't, use + * doRollback() instead. + * @param string $fromP Name of the user whose edits to rollback. + * @param string $summary Custom summary. Set to default summary if empty. + * @param bool $bot If true, mark all reverted edits as bot. + * + * @param array $resultDetails Contains result-specific array of additional values + * @param User $guser The user performing the rollback + * @return array + */ + public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) { + global $wgUseRCPatrol, $wgContLang; + + $dbw = wfGetDB( DB_MASTER ); + + if ( wfReadOnly() ) { + return array( array( 'readonlytext' ) ); + } + + // Get the last editor + $current = $this->getRevision(); + if ( is_null( $current ) ) { + // Something wrong... no page? + return array( array( 'notanarticle' ) ); + } + + $from = str_replace( '_', ' ', $fromP ); + // User name given should match up with the top revision. + // If the user was deleted then $from should be empty. + if ( $from != $current->getUserText() ) { + $resultDetails = array( 'current' => $current ); + return array( array( 'alreadyrolled', + htmlspecialchars( $this->mTitle->getPrefixedText() ), + htmlspecialchars( $fromP ), + htmlspecialchars( $current->getUserText() ) + ) ); + } + + // Get the last edit not by this guy... + // Note: these may not be public values + $user = intval( $current->getRawUser() ); + $user_text = $dbw->addQuotes( $current->getRawUserText() ); + $s = $dbw->selectRow( 'revision', + array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), + array( 'rev_page' => $current->getPage(), + "rev_user != {$user} OR rev_user_text != {$user_text}" + ), __METHOD__, + array( 'USE INDEX' => 'page_timestamp', + 'ORDER BY' => 'rev_timestamp DESC' ) + ); + if ( $s === false ) { + // No one else ever edited this page + return array( array( 'cantrollback' ) ); + } elseif ( $s->rev_deleted & Revision::DELETED_TEXT + || $s->rev_deleted & Revision::DELETED_USER + ) { + // Only admins can see this text + return array( array( 'notvisiblerev' ) ); + } + + // Set patrolling and bot flag on the edits, which gets rollbacked. + // This is done before the rollback edit to have patrolling also on failure (bug 62157). + $set = array(); + if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { + // Mark all reverted edits as bot + $set['rc_bot'] = 1; + } + + if ( $wgUseRCPatrol ) { + // Mark all reverted edits as patrolled + $set['rc_patrolled'] = 1; + } + + if ( count( $set ) ) { + $dbw->update( 'recentchanges', $set, + array( /* WHERE */ + 'rc_cur_id' => $current->getPage(), + 'rc_user_text' => $current->getUserText(), + 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ), + ), __METHOD__ + ); + } + + // Generate the edit summary if necessary + $target = Revision::newFromId( $s->rev_id ); + if ( empty( $summary ) ) { + if ( $from == '' ) { // no public user name + $summary = wfMessage( 'revertpage-nouser' ); + } else { + $summary = wfMessage( 'revertpage' ); + } + } + + // Allow the custom summary to use the same args as the default message + $args = array( + $target->getUserText(), $from, $s->rev_id, + $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ), + $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() ) + ); + if ( $summary instanceof Message ) { + $summary = $summary->params( $args )->inContentLanguage()->text(); + } else { + $summary = wfMsgReplaceArgs( $summary, $args ); + } + + // Trim spaces on user supplied text + $summary = trim( $summary ); + + // Truncate for whole multibyte characters. + $summary = $wgContLang->truncate( $summary, 255 ); + + // Save + $flags = EDIT_UPDATE; + + if ( $guser->isAllowed( 'minoredit' ) ) { + $flags |= EDIT_MINOR; + } + + if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) { + $flags |= EDIT_FORCE_BOT; + } + + // Actually store the edit + $status = $this->doEditContent( + $target->getContent(), + $summary, + $flags, + $target->getId(), + $guser + ); + + if ( !$status->isOK() ) { + return $status->getErrorsArray(); + } + + // raise error, when the edit is an edit without a new version + if ( empty( $status->value['revision'] ) ) { + $resultDetails = array( 'current' => $current ); + return array( array( 'alreadyrolled', + htmlspecialchars( $this->mTitle->getPrefixedText() ), + htmlspecialchars( $fromP ), + htmlspecialchars( $current->getUserText() ) + ) ); + } + + $revId = $status->value['revision']->getId(); + + wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) ); + + $resultDetails = array( + 'summary' => $summary, + 'current' => $current, + 'target' => $target, + 'newid' => $revId + ); + + return array(); + } + + /** + * The onArticle*() functions are supposed to be a kind of hooks + * which should be called whenever any of the specified actions + * are done. + * + * This is a good place to put code to clear caches, for instance. + * + * This is called on page move and undelete, as well as edit + * + * @param Title $title + */ + public static function onArticleCreate( $title ) { + // Update existence markers on article/talk tabs... + if ( $title->isTalkPage() ) { + $other = $title->getSubjectPage(); + } else { + $other = $title->getTalkPage(); + } + + $other->invalidateCache(); + $other->purgeSquid(); + + $title->touchLinks(); + $title->purgeSquid(); + $title->deleteTitleProtection(); + } + + /** + * Clears caches when article is deleted + * + * @param Title $title + */ + public static function onArticleDelete( $title ) { + // Update existence markers on article/talk tabs... + if ( $title->isTalkPage() ) { + $other = $title->getSubjectPage(); + } else { + $other = $title->getTalkPage(); + } + + $other->invalidateCache(); + $other->purgeSquid(); + + $title->touchLinks(); + $title->purgeSquid(); + + // File cache + HTMLFileCache::clearFileCache( $title ); + InfoAction::invalidateCache( $title ); + + // Messages + if ( $title->getNamespace() == NS_MEDIAWIKI ) { + MessageCache::singleton()->replace( $title->getDBkey(), false ); + } + + // Images + if ( $title->getNamespace() == NS_FILE ) { + $update = new HTMLCacheUpdate( $title, 'imagelinks' ); + $update->doUpdate(); + } + + // User talk pages + if ( $title->getNamespace() == NS_USER_TALK ) { + $user = User::newFromName( $title->getText(), false ); + if ( $user ) { + $user->setNewtalk( false ); + } + } + + // Image redirects + RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); + } + + /** + * Purge caches on page update etc + * + * @param Title $title + * @todo Verify that $title is always a Title object (and never false or + * null), add Title hint to parameter $title. + */ + public static function onArticleEdit( $title ) { + // Invalidate caches of articles which include this page + DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' ); + + // Invalidate the caches of all pages which redirect here + DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' ); + + // Purge squid for this page only + $title->purgeSquid(); + + // Clear file cache for this page only + HTMLFileCache::clearFileCache( $title ); + InfoAction::invalidateCache( $title ); + } + + /**#@-*/ + + /** + * Returns a list of categories this page is a member of. + * Results will include hidden categories + * + * @return TitleArray + */ + public function getCategories() { + $id = $this->getId(); + if ( $id == 0 ) { + return TitleArray::newFromResult( new FakeResultWrapper( array() ) ); + } + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'categorylinks', + array( 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ), + // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes + // as not being aliases, and NS_CATEGORY is numeric + array( 'cl_from' => $id ), + __METHOD__ ); + + return TitleArray::newFromResult( $res ); + } + + /** + * Returns a list of hidden categories this page is a member of. + * Uses the page_props and categorylinks tables. + * + * @return array Array of Title objects + */ + public function getHiddenCategories() { + $result = array(); + $id = $this->getId(); + + if ( $id == 0 ) { + return array(); + } + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), + array( 'cl_to' ), + array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', + 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ), + __METHOD__ ); + + if ( $res !== false ) { + foreach ( $res as $row ) { + $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); + } + } + + return $result; + } + + /** + * Return an applicable autosummary if one exists for the given edit. + * @param string|null $oldtext The previous text of the page. + * @param string|null $newtext The submitted text of the page. + * @param int $flags Bitmask: a bitmask of flags submitted for the edit. + * @return string An appropriate autosummary, or an empty string. + * + * @deprecated since 1.21, use ContentHandler::getAutosummary() instead + */ + public static function getAutosummary( $oldtext, $newtext, $flags ) { + // NOTE: stub for backwards-compatibility. assumes the given text is + // wikitext. will break horribly if it isn't. + + ContentHandler::deprecated( __METHOD__, '1.21' ); + + $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); + $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext ); + $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext ); + + return $handler->getAutosummary( $oldContent, $newContent, $flags ); + } + + /** + * Auto-generates a deletion reason + * + * @param bool &$hasHistory Whether the page has a history + * @return string|bool String containing deletion reason or empty string, or boolean false + * if no revision occurred + */ + public function getAutoDeleteReason( &$hasHistory ) { + return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory ); + } + + /** + * Update all the appropriate counts in the category table, given that + * we've added the categories $added and deleted the categories $deleted. + * + * @param array $added The names of categories that were added + * @param array $deleted The names of categories that were deleted + */ + public function updateCategoryCounts( array $added, array $deleted ) { + $that = $this; + $method = __METHOD__; + $dbw = wfGetDB( DB_MASTER ); + + // Do this at the end of the commit to reduce lock wait timeouts + $dbw->onTransactionPreCommitOrIdle( + function() use ( $dbw, $that, $method, $added, $deleted ) { + $ns = $that->getTitle()->getNamespace(); + + $addFields = array( 'cat_pages = cat_pages + 1' ); + $removeFields = array( 'cat_pages = cat_pages - 1' ); + if ( $ns == NS_CATEGORY ) { + $addFields[] = 'cat_subcats = cat_subcats + 1'; + $removeFields[] = 'cat_subcats = cat_subcats - 1'; + } elseif ( $ns == NS_FILE ) { + $addFields[] = 'cat_files = cat_files + 1'; + $removeFields[] = 'cat_files = cat_files - 1'; + } + + if ( count( $added ) ) { + $insertRows = array(); + foreach ( $added as $cat ) { + $insertRows[] = array( + 'cat_title' => $cat, + 'cat_pages' => 1, + 'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0, + 'cat_files' => ( $ns == NS_FILE ) ? 1 : 0, + ); + } + $dbw->upsert( + 'category', + $insertRows, + array( 'cat_title' ), + $addFields, + $method + ); + } + + if ( count( $deleted ) ) { + $dbw->update( + 'category', + $removeFields, + array( 'cat_title' => $deleted ), + $method + ); + } + + foreach ( $added as $catName ) { + $cat = Category::newFromName( $catName ); + wfRunHooks( 'CategoryAfterPageAdded', array( $cat, $that ) ); + } + + foreach ( $deleted as $catName ) { + $cat = Category::newFromName( $catName ); + wfRunHooks( 'CategoryAfterPageRemoved', array( $cat, $that ) ); + } + } + ); + } + + /** + * Updates cascading protections + * + * @param ParserOutput $parserOutput ParserOutput object for the current version + */ + public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) { + if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { + return; + } + + // templatelinks or imagelinks tables may have become out of sync, + // especially if using variable-based transclusions. + // For paranoia, check if things have changed and if + // so apply updates to the database. This will ensure + // that cascaded protections apply as soon as the changes + // are visible. + + // Get templates from templatelinks and images from imagelinks + $id = $this->getId(); + + $dbLinks = array(); + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks' ), + array( 'tl_namespace', 'tl_title' ), + array( 'tl_from' => $id ), + __METHOD__ + ); + + foreach ( $res as $row ) { + $dbLinks["{$row->tl_namespace}:{$row->tl_title}"] = true; + } + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'imagelinks' ), + array( 'il_to' ), + array( 'il_from' => $id ), + __METHOD__ + ); + + foreach ( $res as $row ) { + $dbLinks[NS_FILE . ":{$row->il_to}"] = true; + } + + // Get templates and images from parser output. + $poLinks = array(); + foreach ( $parserOutput->getTemplates() as $ns => $templates ) { + foreach ( $templates as $dbk => $id ) { + $poLinks["$ns:$dbk"] = true; + } + } + foreach ( $parserOutput->getImages() as $dbk => $id ) { + $poLinks[NS_FILE . ":$dbk"] = true; + } + + // Get the diff + $links_diff = array_diff_key( $poLinks, $dbLinks ); + + if ( count( $links_diff ) > 0 ) { + // Whee, link updates time. + // Note: we are only interested in links here. We don't need to get + // other DataUpdate items from the parser output. + $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); + $u->doUpdate(); + } + } + + /** + * Return a list of templates used by this article. + * Uses the templatelinks table + * + * @deprecated since 1.19; use Title::getTemplateLinksFrom() + * @return array Array of Title objects + */ + public function getUsedTemplates() { + return $this->mTitle->getTemplateLinksFrom(); + } + + /** + * This function is called right before saving the wikitext, + * so we can do things like signatures and links-in-context. + * + * @deprecated since 1.19; use Parser::preSaveTransform() instead + * @param string $text Article contents + * @param User $user User doing the edit + * @param ParserOptions $popts Parser options, default options for + * the user loaded if null given + * @return string Article contents with altered wikitext markup (signatures + * converted, {{subst:}}, templates, etc.) + */ + public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) { + global $wgParser, $wgUser; + + wfDeprecated( __METHOD__, '1.19' ); + + $user = is_null( $user ) ? $wgUser : $user; + + if ( $popts === null ) { + $popts = ParserOptions::newFromUser( $user ); + } + + return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); + } + + /** + * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit + * + * @deprecated since 1.19; use Title::isBigDeletion() instead. + * @return bool + */ + public function isBigDeletion() { + wfDeprecated( __METHOD__, '1.19' ); + return $this->mTitle->isBigDeletion(); + } + + /** + * Get the approximate revision count of this page. + * + * @deprecated since 1.19; use Title::estimateRevisionCount() instead. + * @return int + */ + public function estimateRevisionCount() { + wfDeprecated( __METHOD__, '1.19' ); + return $this->mTitle->estimateRevisionCount(); + } + + /** + * Update the article's restriction field, and leave a log entry. + * + * @deprecated since 1.19 + * @param array $limit Set of restriction keys + * @param string $reason + * @param int &$cascade Set to false if cascading protection isn't allowed. + * @param array $expiry Per restriction type expiration + * @param User $user The user updating the restrictions + * @return bool true on success + */ + public function updateRestrictions( + $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null + ) { + global $wgUser; + + $user = is_null( $user ) ? $wgUser : $user; + + return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK(); + } + + /** + * Returns a list of updates to be performed when this page is deleted. The + * updates should remove any information about this page from secondary data + * stores such as links tables. + * + * @param Content|null $content Optional Content object for determining the + * necessary updates. + * @return array An array of DataUpdates objects + */ + public function getDeletionUpdates( Content $content = null ) { + if ( !$content ) { + // load content object, which may be used to determine the necessary updates + // XXX: the content may not be needed to determine the updates, then this would be overhead. + $content = $this->getContent( Revision::RAW ); + } + + if ( !$content ) { + $updates = array(); + } else { + $updates = $content->getDeletionUpdates( $this ); + } + + wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) ); + return $updates; + } +} diff --git a/includes/poolcounter/PoolWorkArticleView.php b/includes/poolcounter/PoolWorkArticleView.php new file mode 100644 index 0000000000..4cdb0fff20 --- /dev/null +++ b/includes/poolcounter/PoolWorkArticleView.php @@ -0,0 +1,208 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +class PoolWorkArticleView extends PoolCounterWork { + /** @var Page */ + private $page; + + /** @var string */ + private $cacheKey; + + /** @var int */ + private $revid; + + /** @var ParserOptions */ + private $parserOptions; + + /** @var Content|null */ + private $content = null; + + /** @var ParserOutput|bool */ + private $parserOutput = false; + + /** @var bool */ + private $isDirty = false; + + /** @var Status|bool */ + private $error = false; + + /** + * @param Page $page + * @param int $revid ID of the revision being parsed. + * @param bool $useParserCache Whether to use the parser cache. + * @param ParserOptions $parserOptions ParserOptions to use for the parse + * operation. + * @param Content|string $content Content to parse or null to load it; may + * also be given as a wikitext string, for BC. + */ + public function __construct( Page $page, ParserOptions $parserOptions, + $revid, $useParserCache, $content = null + ) { + if ( is_string( $content ) ) { // BC: old style call + $modelId = $page->getRevision()->getContentModel(); + $format = $page->getRevision()->getContentFormat(); + $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format ); + } + + $this->page = $page; + $this->revid = $revid; + $this->cacheable = $useParserCache; + $this->parserOptions = $parserOptions; + $this->content = $content; + $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); + parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); + } + + /** + * Get the ParserOutput from this object, or false in case of failure + * + * @return ParserOutput + */ + public function getParserOutput() { + return $this->parserOutput; + } + + /** + * Get whether the ParserOutput is a dirty one (i.e. expired) + * + * @return bool + */ + public function getIsDirty() { + return $this->isDirty; + } + + /** + * Get a Status object in case of error or false otherwise + * + * @return Status|bool + */ + public function getError() { + return $this->error; + } + + /** + * @return bool + */ + public function doWork() { + global $wgUseFileCache; + + // @todo several of the methods called on $this->page are not declared in Page, but present + // in WikiPage and delegated by Article. + + $isCurrent = $this->revid === $this->page->getLatest(); + + if ( $this->content !== null ) { + $content = $this->content; + } elseif ( $isCurrent ) { + // XXX: why use RAW audience here, and PUBLIC (default) below? + $content = $this->page->getContent( Revision::RAW ); + } else { + $rev = Revision::newFromTitle( $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 ( $content === null ) { + return false; + } + + // Reduce effects of race conditions for slow parses (bug 46014) + $cacheTime = wfTimestampNow(); + + $time = - microtime( true ); + $this->parserOutput = $content->getParserOutput( + $this->page->getTitle(), + $this->revid, + $this->parserOptions + ); + $time += microtime( true ); + + // Timing hack + if ( $time > 3 ) { + wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, + $this->page->getTitle()->getPrefixedDBkey() ) ); + } + + if ( $this->cacheable && $this->parserOutput->isCacheable() && $isCurrent ) { + ParserCache::singleton()->save( + $this->parserOutput, $this->page, $this->parserOptions, $cacheTime, $this->revid ); + } + + // Make sure file cache is not used on uncacheable content. + // Output that has magic words in it can still use the parser cache + // (if enabled), though it will generally expire sooner. + if ( !$this->parserOutput->isCacheable() || $this->parserOutput->containsOldMagic() ) { + $wgUseFileCache = false; + } + + if ( $isCurrent ) { + $this->page->doCascadeProtectionUpdates( $this->parserOutput ); + } + + return true; + } + + /** + * @return bool + */ + public function getCachedWork() { + $this->parserOutput = ParserCache::singleton()->get( $this->page, $this->parserOptions ); + + if ( $this->parserOutput === false ) { + wfDebug( __METHOD__ . ": parser cache miss\n" ); + return false; + } else { + wfDebug( __METHOD__ . ": parser cache hit\n" ); + return true; + } + } + + /** + * @return bool + */ + public function fallback() { + $this->parserOutput = ParserCache::singleton()->getDirty( $this->page, $this->parserOptions ); + + if ( $this->parserOutput === false ) { + wfDebugLog( 'dirty', 'dirty missing' ); + wfDebug( __METHOD__ . ": no dirty cache\n" ); + return false; + } else { + wfDebug( __METHOD__ . ": sending dirty output\n" ); + wfDebugLog( 'dirty', "dirty output {$this->cacheKey}" ); + $this->isDirty = true; + return true; + } + } + + /** + * @param Status $status + * @return bool + */ + public function error( $status ) { + $this->error = $status; + return false; + } +}