+++ /dev/null
-<?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 );
- }
- // ******
-}
'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',
'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',
'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',
'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',
'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',
'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',
'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',
+++ /dev/null
-<?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() );
- }
-}
+++ /dev/null
-<?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;
- }
-
-}
+++ /dev/null
-<?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;
- }
-}
+++ /dev/null
-<?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 );
- }
-}
+++ /dev/null
-<?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;
- }
-}
--- /dev/null
+<?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 );
+ }
+ // ******
+}
--- /dev/null
+<?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() );
+ }
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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 );
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}