diff options
Diffstat (limited to 'includes')
509 files changed, 65551 insertions, 33821 deletions
diff --git a/includes/Action.php b/includes/Action.php new file mode 100644 index 00000000..d5432b23 --- /dev/null +++ b/includes/Action.php @@ -0,0 +1,467 @@ +<?php +/** + * Actions are things which can be done to pages (edit, delete, rollback, etc). They + * are distinct from Special Pages because an action must apply to exactly one page. + * + * To add an action in an extension, create a subclass of Action, and add the key to + * $wgActions. There is also the deprecated UnknownAction hook + * + * + * 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 + * + * @file + */ +abstract class Action { + + /** + * Page on which we're performing the action + * @var Article + */ + protected $page; + + /** + * IContextSource if specified; otherwise we'll use the Context from the Page + * @var IContextSource + */ + protected $context; + + /** + * The fields used to create the HTMLForm + * @var Array + */ + protected $fields; + + /** + * Get the Action subclass which should be used to handle this action, false if + * the action is disabled, or null if it's not recognised + * @param $action String + * @param $overrides Array + * @return bool|null|string + */ + private final static function getClass( $action, array $overrides ) { + global $wgActions; + $action = strtolower( $action ); + + if ( !isset( $wgActions[$action] ) ) { + return null; + } + + if ( $wgActions[$action] === false ) { + return false; + } elseif ( $wgActions[$action] === true && isset( $overrides[$action] ) ) { + return $overrides[$action]; + } elseif ( $wgActions[$action] === true ) { + return ucfirst( $action ) . 'Action'; + } else { + return $wgActions[$action]; + } + } + + /** + * Get an appropriate Action subclass for the given action + * @param $action String + * @param $page Article + * @return Action|false|null false if the action is disabled, null + * if it is not recognised + */ + public final static function factory( $action, Page $page ) { + $class = self::getClass( $action, $page->getActionOverrides() ); + if ( $class ) { + $obj = new $class( $page ); + return $obj; + } + return $class; + } + + /** + * Check if a given action is recognised, even if it's disabled + * + * @param $name String: name of an action + * @return Bool + */ + public final static function exists( $name ) { + return self::getClass( $name ) !== null; + } + + /** + * Get the IContextSource in use here + * @return IContextSource + */ + protected final function getContext() { + if ( $this->context instanceof IContextSource ) { + return $this->context; + } + return $this->page->getContext(); + } + + /** + * Get the WebRequest being used for this instance + * + * @return WebRequest + */ + protected final function getRequest() { + return $this->getContext()->getRequest(); + } + + /** + * Get the OutputPage being used for this instance + * + * @return OutputPage + */ + protected final function getOutput() { + return $this->getContext()->getOutput(); + } + + /** + * Shortcut to get the User being used for this instance + * + * @return User + */ + protected final function getUser() { + return $this->getContext()->getUser(); + } + + /** + * Shortcut to get the Skin being used for this instance + * + * @return Skin + */ + protected final function getSkin() { + return $this->getContext()->getSkin(); + } + + /** + * Shortcut to get the user Language being used for this instance + * + * @return Skin + */ + protected final function getLang() { + return $this->getContext()->getLang(); + } + + /** + * Shortcut to get the Title object from the page + * @return Title + */ + protected final function getTitle() { + return $this->page->getTitle(); + } + + /** + * Protected constructor: use Action::factory( $action, $page ) to actually build + * these things in the real world + * @param Page $page + */ + protected function __construct( Page $page ) { + $this->page = $page; + } + + /** + * Return the name of the action this object responds to + * @return String lowercase + */ + public abstract function getName(); + + /** + * Get the permission required to perform this action. Often, but not always, + * the same as the action name + */ + public abstract function getRestriction(); + + /** + * Checks if the given user (identified by an object) can perform this action. Can be + * overridden by sub-classes with more complicated permissions schemes. Failures here + * must throw subclasses of ErrorPageError + * + * @param $user User: the user to check, or null to use the context user + * @throws ErrorPageError + */ + protected function checkCanExecute( User $user ) { + if ( $this->requiresWrite() && wfReadOnly() ) { + throw new ReadOnlyError(); + } + + if ( $this->getRestriction() !== null && !$user->isAllowed( $this->getRestriction() ) ) { + throw new PermissionsError( $this->getRestriction() ); + } + + if ( $this->requiresUnblock() && $user->isBlocked() ) { + $block = $user->mBlock; + throw new UserBlockedError( $block ); + } + } + + /** + * Whether this action requires the wiki not to be locked + * @return Bool + */ + public function requiresWrite() { + return true; + } + + /** + * Whether this action can still be executed by a blocked user + * @return Bool + */ + public function requiresUnblock() { + return true; + } + + /** + * Set output headers for noindexing etc. This function will not be called through + * the execute() entry point, so only put UI-related stuff in here. + */ + protected function setHeaders() { + $out = $this->getOutput(); + $out->setRobotPolicy( "noindex,nofollow" ); + $out->setPageTitle( $this->getPageTitle() ); + $this->getOutput()->setSubtitle( $this->getDescription() ); + $out->setArticleRelated( true ); + } + + /** + * Returns the name that goes in the \<h1\> page title + * + * @return String + */ + protected function getPageTitle() { + return $this->getTitle()->getPrefixedText(); + } + + /** + * Returns the description that goes below the \<h1\> tag + * + * @return String + */ + protected function getDescription() { + return wfMsg( strtolower( $this->getName() ) ); + } + + /** + * The main action entry point. Do all output for display and send it to the context + * output. Do not use globals $wgOut, $wgRequest, etc, in implementations; use + * $this->getOutput(), etc. + * @throws ErrorPageError + */ + public abstract function show(); + + /** + * Execute the action in a silent fashion: do not display anything or release any errors. + * @param $data Array values that would normally be in the POST request + * @param $captureErrors Bool whether to catch exceptions and just return false + * @return Bool whether execution was successful + */ + public abstract function execute(); +} + +abstract class FormAction extends Action { + + /** + * Get an HTMLForm descriptor array + * @return Array + */ + protected abstract function getFormFields(); + + /** + * Add pre- or post-text to the form + * @return String HTML which will be sent to $form->addPreText() + */ + protected function preText() { return ''; } + protected function postText() { return ''; } + + /** + * Play with the HTMLForm if you need to more substantially + * @param $form HTMLForm + */ + protected function alterForm( HTMLForm $form ) {} + + /** + * Get the HTMLForm to control behaviour + * @return HTMLForm|null + */ + protected function getForm() { + $this->fields = $this->getFormFields(); + + // Give hooks a chance to alter the form, adding extra fields or text etc + wfRunHooks( 'ActionModifyFormFields', array( $this->getName(), &$this->fields, $this->page ) ); + + $form = new HTMLForm( $this->fields, $this->getContext() ); + $form->setSubmitCallback( array( $this, 'onSubmit' ) ); + + // Retain query parameters (uselang etc) + $form->addHiddenField( 'action', $this->getName() ); // Might not be the same as the query string + $params = array_diff_key( + $this->getRequest()->getQueryValues(), + array( 'action' => null, 'title' => null ) + ); + $form->addHiddenField( 'redirectparams', wfArrayToCGI( $params ) ); + + $form->addPreText( $this->preText() ); + $form->addPostText( $this->postText() ); + $this->alterForm( $form ); + + // Give hooks a chance to alter the form, adding extra fields or text etc + wfRunHooks( 'ActionBeforeFormDisplay', array( $this->getName(), &$form, $this->page ) ); + + return $form; + } + + /** + * Process the form on POST submission. If you return false from getFormFields(), + * this will obviously never be reached. If you don't want to do anything with the + * form, just return false here + * @param $data Array + * @return Bool|Array true for success, false for didn't-try, array of errors on failure + */ + public abstract function onSubmit( $data ); + + /** + * Do something exciting on successful processing of the form. This might be to show + * a confirmation message (watch, rollback, etc) or to redirect somewhere else (edit, + * protect, etc). + */ + public abstract function onSuccess(); + + /** + * The basic pattern for actions is to display some sort of HTMLForm UI, maybe with + * some stuff underneath (history etc); to do some processing on submission of that + * form (delete, protect, etc) and to do something exciting on 'success', be that + * display something new or redirect to somewhere. Some actions have more exotic + * behaviour, but that's what subclassing is for :D + */ + public function show() { + $this->setHeaders(); + + // This will throw exceptions if there's a problem + $this->checkCanExecute( $this->getUser() ); + + $form = $this->getForm(); + if ( $form->show() ) { + $this->onSuccess(); + } + } + + /** + * @see Action::execute() + * @throws ErrorPageError + * @param array|null $data + * @param bool $captureErrors + * @return bool + */ + public function execute( array $data = null, $captureErrors = true ) { + try { + // Set a new context so output doesn't leak. + $this->context = clone $this->page->getContext(); + + // This will throw exceptions if there's a problem + $this->checkCanExecute( $this->getUser() ); + + $fields = array(); + foreach ( $this->fields as $key => $params ) { + if ( isset( $data[$key] ) ) { + $fields[$key] = $data[$key]; + } elseif ( isset( $params['default'] ) ) { + $fields[$key] = $params['default']; + } else { + $fields[$key] = null; + } + } + $status = $this->onSubmit( $fields ); + if ( $status === true ) { + // This might do permanent stuff + $this->onSuccess(); + return true; + } else { + return false; + } + } + catch ( ErrorPageError $e ) { + if ( $captureErrors ) { + return false; + } else { + throw $e; + } + } + } +} + +/** + * Actions generally fall into two groups: the show-a-form-then-do-something-with-the-input + * format (protect, delete, move, etc), and the just-do-something format (watch, rollback, + * patrol, etc). + */ +abstract class FormlessAction extends Action { + + /** + * Show something on GET request. + * @return String|null will be added to the HTMLForm if present, or just added to the + * output if not. Return null to not add anything + */ + public abstract function onView(); + + /** + * We don't want an HTMLForm + */ + protected function getFormFields() { + return false; + } + + public function onSubmit( $data ) { + return false; + } + + public function onSuccess() { + return false; + } + + public function show() { + $this->setHeaders(); + + // This will throw exceptions if there's a problem + $this->checkCanExecute( $this->getUser() ); + + $this->getOutput()->addHTML( $this->onView() ); + } + + /** + * Execute the action silently, not giving any output. Since these actions don't have + * forms, they probably won't have any data, but some (eg rollback) may do + * @param $data Array values that would normally be in the GET request + * @param $captureErrors Bool whether to catch exceptions and just return false + * @return Bool whether execution was successful + */ + public function execute( array $data = null, $captureErrors = true ) { + try { + // Set a new context so output doesn't leak. + $this->context = clone $this->page->getContext(); + if ( is_array( $data ) ) { + $this->context->setRequest( new FauxRequest( $data, false ) ); + } + + // This will throw exceptions if there's a problem + $this->checkCanExecute( $this->getUser() ); + + $this->onView(); + return true; + } + catch ( ErrorPageError $e ) { + if ( $captureErrors ) { + return false; + } else { + throw $e; + } + } + } +} diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index f7583188..17b154d6 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -7,12 +7,6 @@ * Handle ajax requests and send them to the proper handler. */ -if ( !( defined( 'MEDIAWIKI' ) && $wgUseAjax ) ) { - die( 1 ); -} - -require_once( 'AjaxFunctions.php' ); - /** * Object-Oriented Ajax functions. * @ingroup Ajax @@ -74,7 +68,7 @@ class AjaxDispatcher { * request. */ function performAction() { - global $wgAjaxExportList, $wgOut, $wgUser; + global $wgAjaxExportList, $wgOut; if ( empty( $this->mode ) ) { return; @@ -90,13 +84,6 @@ class AjaxDispatcher { 'Bad Request', "unknown function " . (string) $this->func_name ); - } elseif ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) - && !$wgUser->isAllowed( 'read' ) ) - { - wfHttpError( - 403, - 'Forbidden', - 'You must log in to view pages.' ); } else { wfDebug( __METHOD__ . ' dispatching ' . $this->func_name . "\n" ); diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php deleted file mode 100644 index 8e5de31b..00000000 --- a/includes/AjaxFunctions.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php -/** - * Handler functions for Ajax requests - * - * @file - * @ingroup Ajax - */ - -if ( !defined( 'MEDIAWIKI' ) ) { - die( 1 ); -} - -/** - * Function converts an Javascript escaped string back into a string with - * specified charset (default is UTF-8). - * Modified function from http://pure-essence.net/stuff/code/utf8RawUrlDecode.phps - * - * @param $source String escaped with Javascript's escape() function - * @param $iconv_to String destination character set will be used as second parameter - * in the iconv function. Default is UTF-8. - * @return string - */ -function js_unescape( $source, $iconv_to = 'UTF-8' ) { - $decodedStr = ''; - $pos = 0; - $len = strlen ( $source ); - - while ( $pos < $len ) { - $charAt = substr ( $source, $pos, 1 ); - if ( $charAt == '%' ) { - $pos++; - $charAt = substr ( $source, $pos, 1 ); - - if ( $charAt == 'u' ) { - // we got a unicode character - $pos++; - $unicodeHexVal = substr ( $source, $pos, 4 ); - $unicode = hexdec ( $unicodeHexVal ); - $decodedStr .= code2utf( $unicode ); - $pos += 4; - } else { - // we have an escaped ascii character - $hexVal = substr ( $source, $pos, 2 ); - $decodedStr .= chr ( hexdec ( $hexVal ) ); - $pos += 2; - } - } else { - $decodedStr .= $charAt; - $pos++; - } - } - - if ( $iconv_to != "UTF-8" ) { - $decodedStr = iconv( "utf-8", $iconv_to, $decodedStr ); - } - - return $decodedStr; -} - -/** - * Function coverts number of utf char into that character. - * Function taken from: http://www.php.net/manual/en/function.utf8-encode.php#49336 - * - * @param $num Integer - * @return utf8char - */ -function code2utf( $num ) { - if ( $num < 128 ) { - return chr( $num ); - } - - if ( $num < 2048 ) { - return chr( ( $num >> 6 ) + 192 ) . chr( ( $num&63 ) + 128 ); - } - - if ( $num < 65536 ) { - return chr( ( $num >> 12 ) + 224 ) . chr( ( ( $num >> 6 )&63 ) + 128 ) . chr( ( $num&63 ) + 128 ); - } - - if ( $num < 2097152 ) { - return chr( ( $num >> 18 ) + 240 ) . chr( ( ( $num >> 12 )&63 ) + 128 ) . chr( ( ( $num >> 6 )&63 ) + 128 ) . chr( ( $num&63 ) + 128 ); - } - - return ''; -} - -/** - * Called in some places (currently just extensions) - * to get the URL for a given file. - */ -function wfAjaxGetFileUrl( $file ) { - $file = wfFindFile( $file ); - - if ( !$file || !$file->exists() ) { - return null; - } - - $url = $file->getUrl(); - - return $url; -} diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 014798f8..b9f80855 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -6,10 +6,6 @@ * @ingroup Ajax */ -if ( !defined( 'MEDIAWIKI' ) ) { - die( 1 ); -} - /** * Handle responses for Ajax requests (send headers, print * content, that sort of thing) diff --git a/includes/Article.php b/includes/Article.php index 3e8cfd5e..a0cc6a95 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -5,7 +5,11 @@ */ /** - * Class representing a MediaWiki article and history. + * 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 @@ -13,198 +17,125 @@ * * @internal documentation reviewed 15 Mar 2010 */ -class Article { +class Article extends Page { /**@{{ * @private */ - var $mComment = ''; // !< + + /** + * @var IContextSource + */ + protected $mContext; + + /** + * @var WikiPage + */ + protected $mPage; + var $mContent; // !< var $mContentLoaded = false; // !< - var $mCounter = -1; // !< Not loaded - var $mCurID = -1; // !< Not loaded - var $mDataLoaded = false; // !< - var $mForUpdate = false; // !< - var $mGoodAdjustment = 0; // !< - var $mIsRedirect = false; // !< - var $mLatest = false; // !< - var $mMinorEdit; // !< var $mOldId; // !< - var $mPreparedEdit = false; // !< Title object if set - var $mRedirectedFrom = null; // !< Title object if set - var $mRedirectTarget = null; // !< Title object if set + + /** + * @var Title + */ + var $mRedirectedFrom = null; + + /** + * @var mixed: boolean false or URL string + */ var $mRedirectUrl = false; // !< var $mRevIdFetched = 0; // !< - var $mRevision; // !< Revision object if set - var $mTimestamp = ''; // !< - var $mTitle; // !< Title object - var $mTotalAdjustment = 0; // !< - var $mTouched = '19700101000000'; // !< - var $mUser = -1; // !< Not loaded - var $mUserText = ''; // !< username from Revision if set - var $mParserOptions; // !< ParserOptions object - var $mParserOutput; // !< ParserCache object if set + + /** + * @var Revision + */ + var $mRevision = null; + + /** + * @var ParserOutput + */ + var $mParserOutput; + /**@}}*/ /** * Constructor and clear the article - * @param $title Reference to a Title object. + * @param $title Title Reference to a Title object. * @param $oldId Integer revision ID, null to fetch from request, zero for current */ public function __construct( Title $title, $oldId = null ) { - // FIXME: does the reference play any role here? - $this->mTitle =& $title; $this->mOldId = $oldId; + $this->mPage = $this->newPage( $title ); + } + + protected function newPage( Title $title ) { + return new WikiPage( $title ); } /** - * Constructor from an page id - * @param $id The article ID to load + * Constructor from a page id + * @param $id Int article ID to load */ public static function newFromID( $id ) { $t = Title::newFromID( $id ); - # FIXME: doesn't inherit right + # @todo FIXME: Doesn't inherit right return $t == null ? null : new self( $t ); # return $t == null ? null : new static( $t ); // PHP 5.3 } /** - * Tell the page view functions that this view was redirected - * from another page on the wiki. - * @param $from Title object. - */ - public function setRedirectedFrom( Title $from ) { - $this->mRedirectedFrom = $from; - } - - /** - * If this page is a redirect, get its target + * Create an Article object of the appropriate class for the given page. * - * The target will be fetched from the redirect table if possible. - * If this page doesn't have an entry there, call insertRedirect() - * @return mixed Title object, or null if this page is not a redirect + * @param $title Title + * @param $context IContextSource + * @return Article object */ - public function getRedirectTarget() { - if ( !$this->mTitle->isRedirect() ) { - return null; + public static function newFromTitle( $title, IContextSource $context ) { + if ( NS_MEDIA == $title->getNamespace() ) { + // FIXME: where should this go? + $title = Title::makeTitle( NS_FILE, $title->getDBkey() ); } - 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 ) ) { - return $this->mRedirectTarget = Title::makeTitle( - $row->rd_namespace, $row->rd_title, - $row->rd_fragment, $row->rd_interwiki ); + $page = null; + wfRunHooks( 'ArticleFromTitle', array( &$title, &$page ) ); + 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 ); - # This page doesn't have an entry in the redirect table - return $this->mRedirectTarget = $this->insertRedirect(); + return $page; } /** - * Insert an entry for this page into the redirect table. + * Create an Article object of the appropriate class for the given page. * - * Don't call this function directly unless you know what you're doing. - * @return Title object or null if not a redirect + * @param $page WikiPage + * @param $context IContextSource + * @return Article object */ - public function insertRedirect() { - // recurse through to only get the final target - $retval = Title::newFromRedirectRecurse( $this->getContent() ); - 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 $rt Title 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__ - ); + 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; } /** - * Get the Title object or URL this page redirects to - * - * @return mixed false, Title of in-wiki target, or string with URL - */ - public function followRedirect() { - return $this->getRedirectURL( $this->getRedirectTarget() ); - } - - /** - * Get the Title object this text redirects to - * - * @param $text string article content containing redirect info - * @return mixed false, Title of in-wiki target, or string with URL - * @deprecated - */ - public function followRedirectText( $text ) { - // recurse through to only get the final target - return $this->getRedirectURL( Title::newFromRedirectRecurse( $text ) ); - } - - /** - * 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 $rt Title Redirect target - * @return mixed false, Title object of local target, or string with URL + * Tell the page view functions that this view was redirected + * from another page on the wiki. + * @param $from Title object. */ - public function getRedirectURL( $rt ) { - if ( $rt ) { - if ( $rt->getInterwiki() != '' ) { - 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( 'rdfrom=' . urlencode( $source ) ); - } - } else { - if ( $rt->getNamespace() == NS_SPECIAL ) { - // Gotta handle redirects to special pages differently: - // Fill the HTTP response "Location" header and ignore - // the rest of the page we're on. - // - // This can be hard to reverse, so they may be disabled. - if ( $rt->isSpecial( 'Userlogout' ) ) { - // rolleyes - } else { - return $rt->getFullURL(); - } - } - - return $rt; - } - } - - // No or invalid redirect - return false; + public function setRedirectedFrom( Title $from ) { + $this->mRedirectedFrom = $from; } /** @@ -212,31 +143,22 @@ class Article { * @return Title object of this page */ public function getTitle() { - return $this->mTitle; + return $this->mPage->getTitle(); } /** * Clear the object - * FIXME: shouldn't this be public? + * @todo FIXME: Shouldn't this be public? * @private */ public function clear() { - $this->mDataLoaded = false; $this->mContentLoaded = false; - $this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded $this->mRedirectedFrom = null; # Title object if set - $this->mRedirectTarget = null; # Title object if set - $this->mUserText = - $this->mTimestamp = $this->mComment = ''; - $this->mGoodAdjustment = $this->mTotalAdjustment = 0; - $this->mTouched = '19700101000000'; - $this->mForUpdate = false; - $this->mIsRedirect = false; $this->mRevIdFetched = 0; $this->mRedirectUrl = false; - $this->mLatest = false; - $this->mPreparedEdit = false; + + $this->mPage->clear(); } /** @@ -250,20 +172,18 @@ class Article { * @return Return the text of this revision */ public function getContent() { - global $wgUser, $wgContLang, $wgMessageCache; + global $wgUser; wfProfileIn( __METHOD__ ); - if ( $this->getID() === 0 ) { + 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->mTitle->getNamespace() == NS_MEDIAWIKI ) { - # If this is a system message, get the default text. - list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) ); - $text = wfMsgGetKey( $message, false, $lang, false ); - - if ( wfEmptyMsg( $message, $text ) ) + if ( $this->getTitle()->getNamespace() == NS_MEDIAWIKI ) { + $text = $this->getTitle()->getDefaultMessageText(); + if ( $text === false ) { $text = ''; + } } else { $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' ); } @@ -279,71 +199,6 @@ class Article { } /** - * Get the text of the current revision. No side-effects... - * - * @return Return the text of the current revision - */ - public function getRawText() { - // Check process cache for current revision - if ( $this->mContentLoaded && $this->mOldId == 0 ) { - return $this->mContent; - } - - $rev = Revision::newFromTitle( $this->mTitle ); - $text = $rev ? $rev->getRawText() : false; - - return $text; - } - - /** - * This function returns the text of a section, specified by a number ($section). - * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or - * the first section before any such heading (section 0). - * - * If a section contains subsections, these are also returned. - * - * @param $text String: text to look in - * @param $section Integer: section number - * @return string text of the requested section - * @deprecated - */ - public function getSection( $text, $section ) { - global $wgParser; - return $wgParser->getSection( $text, $section ); - } - - /** - * 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 $undo Revision - * @param $undoafter Revision Must be an earlier revision than $undo - * @return mixed string on success, false on failure - */ - public function getUndoText( Revision $undo, Revision $undoafter = null ) { - $currentRev = Revision::newFromTitle( $this->mTitle ); - if ( !$currentRev ) { - return false; // no page - } - $undo_text = $undo->getText(); - $undoafter_text = $undoafter->getText(); - $cur_text = $currentRev->getText(); - - if ( $cur_text == $undo_text ) { - # No use doing a merge if it's just a straight revert. - return $undoafter_text; - } - - $undone_text = ''; - - if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) { - return false; - } - - return $undone_text; - } - - /** * @return int The oldid of the article that is to be shown, 0 for the * current revision */ @@ -370,14 +225,14 @@ class Article { if ( isset( $oldid ) ) { $oldid = intval( $oldid ); if ( $wgRequest->getVal( 'direction' ) == 'next' ) { - $nextid = $this->mTitle->getNextRevisionID( $oldid ); + $nextid = $this->getTitle()->getNextRevisionID( $oldid ); if ( $nextid ) { $oldid = $nextid; } else { - $this->mRedirectUrl = $this->mTitle->getFullURL( 'redirect=no' ); + $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' ); } } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) { - $previd = $this->mTitle->getPreviousRevisionID( $oldid ); + $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); if ( $previd ) { $oldid = $previd; } @@ -401,102 +256,12 @@ class Article { wfProfileIn( __METHOD__ ); - $oldid = $this->getOldID(); - $this->mOldId = $oldid; - $this->fetchContent( $oldid ); + $this->fetchContent( $this->getOldID() ); wfProfileOut( __METHOD__ ); } /** - * Fetch a page record with the given conditions - * @param $dbr Database object - * @param $conditions Array - * @return mixed Database result resource, or false on failure - */ - protected function pageData( $dbr, $conditions ) { - $fields = array( - 'page_id', - 'page_namespace', - 'page_title', - 'page_restrictions', - 'page_counter', - 'page_is_redirect', - 'page_is_new', - 'page_random', - 'page_touched', - 'page_latest', - 'page_len', - ); - - wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); - - $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__ ); - - 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 $dbr Database object - * @param $title Title object - * @return mixed Database result resource, or false on failure - */ - public function pageDataFromTitle( $dbr, $title ) { - return $this->pageData( $dbr, array( - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() ) ); - } - - /** - * Fetch a page record matching the requested ID - * - * @param $dbr Database - * @param $id Integer - */ - protected function pageDataFromId( $dbr, $id ) { - return $this->pageData( $dbr, array( 'page_id' => $id ) ); - } - - /** - * Set the general counter, title etc data loaded from - * some source. - * - * @param $data Database row object or "fromdb" - */ - public function loadPageData( $data = 'fromdb' ) { - if ( $data === 'fromdb' ) { - $dbr = wfGetDB( DB_MASTER ); - $data = $this->pageDataFromId( $dbr, $this->getId() ); - } - - $lc = LinkCache::singleton(); - - if ( $data ) { - $lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect, $data->page_latest ); - - $this->mTitle->mArticleID = intval( $data->page_id ); - - # Old-fashioned restrictions - $this->mTitle->loadRestrictions( $data->page_restrictions ); - - $this->mCounter = intval( $data->page_counter ); - $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); - $this->mIsRedirect = intval( $data->page_is_redirect ); - $this->mLatest = intval( $data->page_latest ); - } else { - $lc->addBadLinkObj( $this->mTitle ); - $this->mTitle->mArticleID = 0; - } - - $this->mDataLoaded = true; - } - - /** * Get text of an article from database * Does *NOT* follow redirects. * @@ -508,57 +273,44 @@ class Article { return $this->mContent; } - $dbr = wfGetDB( DB_MASTER ); - # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. - $t = $this->mTitle->getPrefixedText(); + $t = $this->getTitle()->getPrefixedText(); $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : ''; $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ; if ( $oldid ) { $revision = Revision::newFromId( $oldid ); - if ( $revision === null ) { + if ( !$revision ) { wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" ); return false; } - - $data = $this->pageDataFromId( $dbr, $revision->getPage() ); - - if ( !$data ) { - wfDebug( __METHOD__ . " failed to get page data linked to revision id $oldid\n" ); - return false; - } - - $this->mTitle = Title::makeTitle( $data->page_namespace, $data->page_title ); - $this->loadPageData( $data ); - } else { - if ( !$this->mDataLoaded ) { - $data = $this->pageDataFromTitle( $dbr, $this->mTitle ); - - if ( !$data ) { - wfDebug( __METHOD__ . " failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" ); + // Revision title doesn't match the page title given? + if ( $this->mPage->getID() != $revision->getPage() ) { + $function = array( get_class( $this->mPage ), 'newFromID' ); + $this->mPage = call_user_func( $function, $revision->getPage() ); + if ( !$this->mPage->getId() ) { + wfDebug( __METHOD__ . " failed to get page data linked to revision id $oldid\n" ); return false; } - - $this->loadPageData( $data ); } - $revision = Revision::newFromId( $this->mLatest ); - if ( $revision === null ) { - wfDebug( __METHOD__ . " failed to retrieve current page, rev_id {$this->mLatest}\n" ); + } else { + if ( !$this->mPage->getLatest() ) { + wfDebug( __METHOD__ . " failed to find page data for title " . $this->getTitle()->getPrefixedText() . "\n" ); + return false; + } + + $revision = $this->mPage->getRevision(); + if ( !$revision ) { + wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" ); return false; } } - // FIXME: Horrible, horrible! This content-loading interface just plain sucks. + // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks. // We should instead work with the Revision object when we need it... $this->mContent = $revision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed - $this->mUser = $revision->getUser(); - $this->mUserText = $revision->getUserText(); - $this->mComment = $revision->getComment(); - $this->mTimestamp = wfTimestamp( TS_MW, $revision->getTimestamp() ); - $this->mRevIdFetched = $revision->getId(); $this->mContentLoaded = true; $this->mRevision =& $revision; @@ -569,118 +321,11 @@ class Article { } /** - * Read/write accessor to select FOR UPDATE - * - * @param $x Mixed: FIXME - * @return mixed value of $x, or value stored in Article::mForUpdate - */ - public function forUpdate( $x = null ) { - return wfSetVar( $this->mForUpdate, $x ); - } - - /** - * Get options for all SELECT statements - * - * @param $options Array: an optional options array which'll be appended to - * the default - * @return Array: options - */ - protected function getSelectOptions( $options = '' ) { - if ( $this->mForUpdate ) { - if ( is_array( $options ) ) { - $options[] = 'FOR UPDATE'; - } else { - $options = 'FOR UPDATE'; - } - } - - return $options; - } - - /** - * @return int Page ID - */ - public function getID() { - return $this->mTitle->getArticleID(); - } - - /** - * @return bool Whether or not the page exists in the database + * No-op + * @deprecated since 1.18 */ - public function exists() { - return $this->getId() > 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 ( -1 == $this->mCounter ) { - $id = $this->getID(); - - if ( $id == 0 ) { - $this->mCounter = 0; - } else { - $dbr = wfGetDB( DB_SLAVE ); - $this->mCounter = $dbr->selectField( 'page', - 'page_counter', - array( 'page_id' => $id ), - __METHOD__, - $this->getSelectOptions() - ); - } - } - - return $this->mCounter; - } - - /** - * 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 $text String: text to analyze - * @return bool - */ - public function isCountable( $text ) { - global $wgUseCommaCount; - - $token = $wgUseCommaCount ? ',' : '[['; - - return $this->mTitle->isContentPage() && !$this->isRedirect( $text ) && in_string( $token, $text ); - } - - /** - * Tests if the article text represents a redirect - * - * @param $text mixed string containing article contents, or boolean - * @return bool - */ - public function isRedirect( $text = false ) { - if ( $text === false ) { - if ( $this->mDataLoaded ) { - return $this->mIsRedirect; - } - - // Apparently loadPageData was never called - $this->loadContent(); - $titleObj = Title::newFromRedirectRecurse( $this->fetchContent() ); - } else { - $titleObj = Title::newFromRedirect( $text ); - } - - return $titleObj !== null; + public function forUpdate() { + wfDeprecated( __METHOD__ ); } /** @@ -694,80 +339,7 @@ class Article { return true; } - return $this->exists() && isset( $this->mRevision ) && $this->mRevision->isCurrent(); - } - - /** - * Loads everything except the text - * This isn't necessary for all uses, so it's only done if needed. - */ - protected function loadLastEdit() { - if ( -1 != $this->mUser ) { - return; - } - - # New or non-existent articles have no user information - $id = $this->getID(); - if ( 0 == $id ) { - return; - } - - $this->mLastRevision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id ); - if ( !is_null( $this->mLastRevision ) ) { - $this->mUser = $this->mLastRevision->getUser(); - $this->mUserText = $this->mLastRevision->getUserText(); - $this->mTimestamp = $this->mLastRevision->getTimestamp(); - $this->mComment = $this->mLastRevision->getComment(); - $this->mMinorEdit = $this->mLastRevision->isMinor(); - $this->mRevIdFetched = $this->mLastRevision->getId(); - } - } - - /** - * @return string GMT timestamp of last article revision - **/ - - public function getTimestamp() { - // Check if the field has been filled by ParserCache::get() - if ( !$this->mTimestamp ) { - $this->loadLastEdit(); - } - - return wfTimestamp( TS_MW, $this->mTimestamp ); - } - - /** - * @return int user ID for the user that made the last article revision - */ - public function getUser() { - $this->loadLastEdit(); - return $this->mUser; - } - - /** - * @return string username of the user that made the last article revision - */ - public function getUserText() { - $this->loadLastEdit(); - return $this->mUserText; - } - - /** - * @return string Comment stored for the last article revision - */ - public function getComment() { - $this->loadLastEdit(); - return $this->mComment; - } - - /** - * Returns true if last revision was marked as "minor edit" - * - * @return boolean Minor edit indicator for the last article revision. - */ - public function getMinorEdit() { - $this->loadLastEdit(); - return $this->mMinorEdit; + return $this->mPage->exists() && $this->mRevision && $this->mRevision->isCurrent(); } /** @@ -776,52 +348,11 @@ class Article { * @return int revision ID of last article revision */ public function getRevIdFetched() { - $this->loadLastEdit(); - return $this->mRevIdFetched; - } - - /** - * FIXME: this does what? - * @param $limit Integer: default 0. - * @param $offset Integer: default 0. - * @return UserArrayFromResult object with User objects of article contributors for requested range - */ - public function getContributors( $limit = 0, $offset = 0 ) { - # FIXME: this is expensive; cache this info somewhere. - - $dbr = wfGetDB( DB_SLAVE ); - $revTable = $dbr->tableName( 'revision' ); - $userTable = $dbr->tableName( 'user' ); - - $pageId = $this->getId(); - - $user = $this->getUser(); - - if ( $user ) { - $excludeCond = "AND rev_user != $user"; + if ( $this->mRevIdFetched ) { + return $this->mRevIdFetched; } else { - $userText = $dbr->addQuotes( $this->getUserText() ); - $excludeCond = "AND rev_user_text != $userText"; + return $this->mPage->getLatest(); } - - $deletedBit = $dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER ); // username hidden? - - $sql = "SELECT {$userTable}.*, rev_user_text as user_name, MAX(rev_timestamp) as timestamp - FROM $revTable LEFT JOIN $userTable ON rev_user = user_id - WHERE rev_page = $pageId - $excludeCond - AND $deletedBit = 0 - GROUP BY rev_user, rev_user_text - ORDER BY timestamp DESC"; - - if ( $limit > 0 ) { - $sql = $dbr->limitResult( $sql, $limit, $offset ); - } - - $sql .= ' ' . $this->getSelectOptions(); - $res = $dbr->query( $sql, __METHOD__ ); - - return new UserArrayFromResult( $res ); } /** @@ -836,67 +367,68 @@ class Article { # Get variables from query string $oldid = $this->getOldID(); + + # getOldID may want us to redirect somewhere else + if ( $this->mRedirectUrl ) { + $wgOut->redirect( $this->mRedirectUrl ); + wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); + wfProfileOut( __METHOD__ ); + + return; + } + + $wgOut->setArticleFlag( true ); + # Set page title (may be overridden by DISPLAYTITLE) + $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() ); + + # If we got diff in the query, we want to see a diff page instead of the article. + if ( $wgRequest->getCheck( 'diff' ) ) { + wfDebug( __METHOD__ . ": showing diff page\n" ); + $this->showDiffPage(); + wfProfileOut( __METHOD__ ); + + return; + } + + # Allow frames by default + $wgOut->allowClickjacking(); + $parserCache = ParserCache::singleton(); - $parserOptions = $this->getParserOptions(); + $parserOptions = $this->mPage->getParserOptions(); # Render printable version, use printable version cache if ( $wgOut->isPrintable() ) { $parserOptions->setIsPrintable( true ); $parserOptions->setEditSection( false ); - } else if ( $wgUseETag && !$this->mTitle->quickUserCan( 'edit' ) ) { + } elseif ( $wgUseETag && !$this->getTitle()->quickUserCan( 'edit' ) ) { $parserOptions->setEditSection( false ); } # Try client and file cache - if ( $oldid === 0 && $this->checkTouched() ) { + if ( $oldid === 0 && $this->mPage->checkTouched() ) { if ( $wgUseETag ) { $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) ); } # Is it client cached? - if ( $wgOut->checkLastModified( $this->getTouched() ) ) { + if ( $wgOut->checkLastModified( $this->mPage->getTouched() ) ) { wfDebug( __METHOD__ . ": done 304\n" ); wfProfileOut( __METHOD__ ); return; # Try file cache - } else if ( $wgUseFileCache && $this->tryFileCache() ) { + } elseif ( $wgUseFileCache && $this->tryFileCache() ) { wfDebug( __METHOD__ . ": done file cache\n" ); # tell wgOut that output is taken care of $wgOut->disable(); - $this->viewUpdates(); + $this->mPage->viewUpdates(); wfProfileOut( __METHOD__ ); return; } } - # getOldID may want us to redirect somewhere else - if ( $this->mRedirectUrl ) { - $wgOut->redirect( $this->mRedirectUrl ); - wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); - wfProfileOut( __METHOD__ ); - - return; - } - - $wgOut->setArticleFlag( true ); - # Set page title (may be overridden by DISPLAYTITLE) - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - - # If we got diff in the query, we want to see a diff page instead of the article. - if ( $wgRequest->getCheck( 'diff' ) ) { - wfDebug( __METHOD__ . ": showing diff page\n" ); - $this->showDiffPage(); - wfProfileOut( __METHOD__ ); - - return; - } - - # Allow frames by default - $wgOut->allowClickjacking(); - - if ( !$wgUseETag && !$this->mTitle->quickUserCan( 'edit' ) ) { + if ( !$wgUseETag && !$this->getTitle()->quickUserCan( 'edit' ) ) { $parserOptions->setEditSection( false ); } @@ -931,14 +463,18 @@ class Article { $wgOut->addParserOutput( $this->mParserOutput ); # Ensure that UI elements requiring revision ID have # the correct version information. - $wgOut->setRevisionId( $this->mLatest ); + $wgOut->setRevisionId( $this->mPage->getLatest() ); $outputDone = true; + # Preload timestamp to avoid a DB hit + if ( isset( $this->mParserOutput->mTimestamp ) ) { + $this->mPage->setTimestamp( $this->mParserOutput->mTimestamp ); + } } } break; case 3: $text = $this->getContent(); - if ( $text === false || $this->getID() == 0 ) { + if ( $text === false || $this->mPage->getID() == 0 ) { wfDebug( __METHOD__ . ": showing missing article\n" ); $this->showMissingArticle(); wfProfileOut( __METHOD__ ); @@ -946,7 +482,7 @@ class Article { } # Another whitelist check in case oldid is altering the title - if ( !$this->mTitle->userCanRead() ) { + if ( !$this->getTitle()->userCanRead() ) { wfDebug( __METHOD__ . ": denied on secondary read check\n" ); $wgOut->loginToUse(); $wgOut->output(); @@ -966,14 +502,14 @@ class Article { } # If this "old" version is the current, then try the parser cache... - if ( $oldid === $this->getLatest() && $this->useParserCache( false ) ) { + if ( $oldid === $this->mPage->getLatest() && $this->useParserCache( false ) ) { $this->mParserOutput = $parserCache->get( $this, $parserOptions ); if ( $this->mParserOutput ) { - wfDebug( __METHOD__ . ": showing parser cache for current rev permalink\n" ); + wfDebug( __METHOD__ . ": showing parser cache for current rev permalink\n" ); $wgOut->addParserOutput( $this->mParserOutput ); - $wgOut->setRevisionId( $this->mLatest ); + $wgOut->setRevisionId( $this->mPage->getLatest() ); $outputDone = true; - break; + break; } } } @@ -983,7 +519,7 @@ class Article { $wgOut->setRevisionId( $this->getRevIdFetched() ); # Pages containing custom CSS or JavaScript get special treatment - if ( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { + if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) { wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); $this->showCssOrJsPage(); $outputDone = true; @@ -995,7 +531,7 @@ class Article { # Don't append the subtitle if this was an old revision $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); # Parse just to get categories, displaytitle, etc. - $this->mParserOutput = $wgParser->parse( $text, $this->mTitle, $parserOptions ); + $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions ); $wgOut->addParserOutputNoText( $this->mParserOutput ); $outputDone = true; } @@ -1007,7 +543,7 @@ class Article { $key = $parserCache->getKey( $this, $parserOptions ); $poolArticleView = new PoolWorkArticleView( $this, $key, $useParserCache, $parserOptions ); - + if ( !$poolArticleView->execute() ) { # Connection or timeout error wfProfileOut( __METHOD__ ); @@ -1035,10 +571,11 @@ class Article { # 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->mTitle->equals( Title::newMainPage() ) - && ( $m = wfMsgForContent( 'pagetitle-view-mainpage' ) ) !== '' ) - { - $wgOut->setHTMLTitle( $m ); + if ( $this->getTitle()->equals( Title::newMainPage() ) ) { + $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage(); + if ( !$msg->isDisabled() ) { + $wgOut->setHTMLTitle( $msg->title( $this->getTitle() )->text() ); + } } # Now that we've filled $this->mParserOutput, we know whether @@ -1048,7 +585,7 @@ class Article { $wgOut->setFollowPolicy( $policy['follow'] ); $this->showViewFooter(); - $this->viewUpdates(); + $this->mPage->viewUpdates(); wfProfileOut( __METHOD__ ); } @@ -1066,16 +603,14 @@ class Article { $unhide = $wgRequest->getInt( 'unhide' ) == 1; $oldid = $this->getOldID(); - $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $unhide ); + $de = new DifferenceEngine( $this->getTitle(), $oldid, $diff, $rcid, $purge, $unhide ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); - // Needed to get the page's current revision - $this->loadPageData(); - if ( $diff == 0 || $diff == $this->mLatest ) { + if ( $diff == 0 || $diff == $this->mPage->getLatest() ) { # Run view updates for current revision only - $this->viewUpdates(); + $this->mPage->viewUpdates(); } } @@ -1087,15 +622,19 @@ class Article { * page views. */ protected function showCssOrJsPage() { - global $wgOut; + global $wgOut, $wgLang; + + $dir = $wgLang->getDir(); + $lang = $wgLang->getCode(); - $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache'>\n$1\n</div>", 'clearyourcache' ); + $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>", + 'clearyourcache' ); // Give hooks a chance to customise the output - if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) { + if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) { // Wrap the whole lot in a <pre> and don't parse $m = array(); - preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); + preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m ); $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); $wgOut->addHTML( "\n</pre>\n" ); @@ -1112,13 +651,12 @@ class Article { global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies; global $wgDefaultRobotPolicy, $wgRequest; - $ns = $this->mTitle->getNamespace(); + $ns = $this->getTitle()->getNamespace(); if ( $ns == NS_USER || $ns == NS_USER_TALK ) { # Don't index user and user talk pages for blocked users (bug 11443) - if ( !$this->mTitle->isSubpage() ) { - $block = new Block(); - if ( $block->load( $this->mTitle->getText() ) ) { + if ( !$this->getTitle()->isSubpage() ) { + if ( Block::newFromTarget( null, $this->getTitle()->getText() ) instanceof Block ) { return array( 'index' => 'noindex', 'follow' => 'nofollow' @@ -1127,7 +665,7 @@ class Article { } } - if ( $this->getID() === 0 || $this->getOldID() ) { + if ( $this->mPage->getID() === 0 || $this->getOldID() ) { # Non-articles (special pages etc), and old revisions return array( 'index' => 'noindex', @@ -1157,7 +695,7 @@ class Article { self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] ) ); } - if ( $this->mTitle->canUseNoindex() && is_object( $this->mParserOutput ) && $this->mParserOutput->getIndexPolicy() ) { + if ( $this->getTitle()->canUseNoindex() && is_object( $this->mParserOutput ) && $this->mParserOutput->getIndexPolicy() ) { # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates # a final sanity check that we have really got the parser output. $policy = array_merge( @@ -1166,11 +704,11 @@ class Article { ); } - if ( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) { + 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->mTitle->getPrefixedText()] ) + self::formatRobotPolicy( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) ); } @@ -1182,7 +720,7 @@ class Article { * merging of several policies using array_merge(). * @param $policy Mixed, returns empty array on null/false/'', transparent * to already-converted arrays, converts String. - * @return associative Array: 'index' => <indexpolicy>, 'follow' => <followpolicy> + * @return Array: 'index' => <indexpolicy>, 'follow' => <followpolicy> */ public static function formatRobotPolicy( $policy ) { if ( is_array( $policy ) ) { @@ -1214,16 +752,15 @@ class Article { * @return boolean */ public function showRedirectedFromHeader() { - global $wgOut, $wgUser, $wgRequest, $wgRedirectSources; + global $wgOut, $wgRequest, $wgRedirectSources; $rdfrom = $wgRequest->getVal( 'rdfrom' ); - $sk = $wgUser->getSkin(); 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 = $sk->link( + $redir = Linker::link( $this->mRedirectedFrom, null, array(), @@ -1235,14 +772,14 @@ class Article { $wgOut->setSubtitle( $s ); // Set the fragment if one was specified in the redirect - if ( strval( $this->mTitle->getFragment() ) != '' ) { - $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() ); + if ( strval( $this->getTitle()->getFragment() ) != '' ) { + $fragment = Xml::escapeJsString( $this->getTitle()->getFragmentForURL() ); $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" ); } // Add a <link rel="canonical"> tag $wgOut->addLink( array( 'rel' => 'canonical', - 'href' => $this->mTitle->getLocalURL() ) + 'href' => $this->getTitle()->getLocalURL() ) ); return true; @@ -1251,7 +788,7 @@ class Article { // 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 = $sk->makeExternalLink( $rdfrom, $rdfrom ); + $redir = Linker::makeExternalLink( $rdfrom, $rdfrom ); $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir ); $wgOut->setSubtitle( $s ); @@ -1269,9 +806,8 @@ class Article { public function showNamespaceHeader() { global $wgOut; - if ( $this->mTitle->isTalkPage() ) { - $msg = wfMsgNoTrans( 'talkpageheader' ); - if ( $msg !== '-' && !wfEmptyMsg( 'talkpageheader', $msg ) ) { + if ( $this->getTitle()->isTalkPage() ) { + if ( !wfMessage( 'talkpageheader' )->isDisabled() ) { $wgOut->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1\n</div>", array( 'talkpageheader' ) ); } } @@ -1284,7 +820,7 @@ class Article { global $wgOut, $wgUseTrackbacks; # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page - if ( $this->mTitle->getNamespace() == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) { + if ( $this->getTitle()->getNamespace() == NS_USER_TALK && IP::isValid( $this->getTitle()->getText() ) ) { $wgOut->addWikiMsg( 'anontalkpagetext' ); } @@ -1296,6 +832,9 @@ class Article { if ( $wgUseTrackbacks ) { $this->addTrackbacks(); } + + wfRunHooks( 'ArticleViewFooter', array( $this ) ); + } /** @@ -1308,11 +847,10 @@ class Article { $rcid = $wgRequest->getVal( 'rcid' ); - if ( !$rcid || !$this->mTitle->quickUserCan( 'patrol' ) ) { + if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol' ) ) { return; } - $sk = $wgUser->getSkin(); $token = $wgUser->editToken( $rcid ); $wgOut->preventClickjacking(); @@ -1320,8 +858,8 @@ class Article { "<div class='patrollink'>" . wfMsgHtml( 'markaspatrolledlink', - $sk->link( - $this->mTitle, + Linker::link( + $this->getTitle(), wfMsgHtml( 'markaspatrolledtext' ), array(), array( @@ -1344,16 +882,16 @@ class Article { global $wgOut, $wgRequest, $wgUser; # Show info in user (talk) namespace. Does the user exist? Is he blocked? - if ( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) { - $parts = explode( '/', $this->mTitle->getText() ); + 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->isLoggedIn() && !$ip ) { # User does not exist $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", - array( 'userpage-userdoesnotexist-view', $rootPart ) ); - } else if ( $user->isBlocked() ) { # Show log extract if the user is currently blocked + array( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ) ); + } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked LogEventsList::showLogExtract( $wgOut, 'block', @@ -1374,7 +912,7 @@ class Article { wfRunHooks( 'ShowMissingArticle', array( $this ) ); # Show delete and move logs - LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle->getPrefixedText(), '', + LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->getTitle()->getPrefixedText(), '', array( 'lim' => 10, 'conds' => array( "log_action != 'revision'" ), 'showIfEmpty' => false, @@ -1385,14 +923,14 @@ class Article { $oldid = $this->getOldID(); if ( $oldid ) { $text = wfMsgNoTrans( 'missing-article', - $this->mTitle->getPrefixedText(), + $this->getTitle()->getPrefixedText(), wfMsgNoTrans( 'missingarticle-rev', $oldid ) ); - } elseif ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) { + } elseif ( $this->getTitle()->getNamespace() === NS_MEDIAWIKI ) { // Use the default message text - $text = $this->getContent(); + $text = $this->getTitle()->getDefaultMessageText(); } else { - $createErrors = $this->mTitle->getUserPermissionsErrors( 'create', $wgUser ); - $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ); + $createErrors = $this->getTitle()->getUserPermissionsErrors( 'create', $wgUser ); + $editErrors = $this->getTitle()->getUserPermissionsErrors( 'edit', $wgUser ); $errors = array_merge( $createErrors, $editErrors ); if ( !count( $errors ) ) { @@ -1403,7 +941,7 @@ class Article { } $text = "<div class='noarticletext'>\n$text\n</div>"; - if ( !$this->hasViewableContent() ) { + if ( !$this->mPage->hasViewableContent() ) { // If there's no backing content, send a 404 Not Found // for better machine handling of broken links. $wgRequest->response()->header( "HTTP/1.1 404 Not Found" ); @@ -1433,10 +971,10 @@ class Article { return false; // If the user needs to confirm that they want to see it... - } else if ( $wgRequest->getInt( 'unhide' ) != 1 ) { + } elseif ( $wgRequest->getInt( 'unhide' ) != 1 ) { # Give explanation and add a link to view the revision... $oldid = intval( $this->getOldID() ); - $link = $this->mTitle->getFullUrl( "oldid={$oldid}&unhide=1" ); + $link = $this->getTitle()->getFullUrl( "oldid={$oldid}&unhide=1" ); $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide'; $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", @@ -1454,41 +992,25 @@ class Article { } /** - * Should the parser cache be used? - * - * @return boolean - */ - public function useParserCache( $oldid ) { - global $wgUser, $wgEnableParserCache; - - return $wgEnableParserCache - && $wgUser->getStubThreshold() == 0 - && $this->exists() - && empty( $oldid ) - && !$this->mTitle->isCssOrJsPage() - && !$this->mTitle->isCssJsSubpage(); - } - - /** * Execute the uncached parse for action=view */ public function doViewParse() { global $wgOut; $oldid = $this->getOldID(); - $parserOptions = $this->getParserOptions(); + $parserOptions = $this->mPage->getParserOptions(); # Render printable version, use printable version cache $parserOptions->setIsPrintable( $wgOut->isPrintable() ); # Don't show section-edit links on old revisions... this way lies madness. - if ( !$this->isCurrent() || $wgOut->isPrintable() || !$this->mTitle->quickUserCan( 'edit' ) ) { + if ( !$this->isCurrent() || $wgOut->isPrintable() || !$this->getTitle()->quickUserCan( 'edit' ) ) { $parserOptions->setEditSection( false ); } - + $useParserCache = $this->useParserCache( $oldid ); $this->outputWikiText( $this->getContent(), $useParserCache, $parserOptions ); - + return true; } @@ -1503,13 +1025,13 @@ class Article { public function tryDirtyCache() { global $wgOut; $parserCache = ParserCache::singleton(); - $options = $this->getParserOptions(); - + $options = $this->mPage->getParserOptions(); + if ( $wgOut->isPrintable() ) { $options->setIsPrintable( true ); $options->setEditSection( false ); } - + $output = $parserCache->getDirty( $this, $options ); if ( $output ) { @@ -1532,48 +1054,46 @@ class Article { /** * View redirect * - * @param $target Title object or Array of destination(s) to redirect + * @param $target Title|Array of destination(s) to redirect * @param $appendSubtitle Boolean [optional] * @param $forceKnown Boolean: 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 ) { - global $wgOut, $wgContLang, $wgStylePath, $wgUser; + global $wgOut, $wgLang, $wgStylePath; if ( !is_array( $target ) ) { $target = array( $target ); } - $imageDir = $wgContLang->getDir(); + $imageDir = $wgLang->getDir(); if ( $appendSubtitle ) { $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) ); } - $sk = $wgUser->getSkin(); // the loop prepends the arrow image before the link, so the first case needs to be outside $title = array_shift( $target ); if ( $forceKnown ) { - $link = $sk->linkKnown( $title, htmlspecialchars( $title->getFullText() ) ); + $link = Linker::linkKnown( $title, htmlspecialchars( $title->getFullText() ) ); } else { - $link = $sk->link( $title, htmlspecialchars( $title->getFullText() ) ); + $link = Linker::link( $title, htmlspecialchars( $title->getFullText() ) ); } $nextRedirect = $wgStylePath . '/common/images/nextredirect' . $imageDir . '.png'; - $alt = $wgContLang->isRTL() ? '←' : '→'; + $alt = $wgLang->isRTL() ? '←' : '→'; // Automatically append redirect=no to each link, since most of them are redirect pages themselves. - // FIXME: where this happens? foreach ( $target as $rt ) { $link .= Html::element( 'img', array( 'src' => $nextRedirect, 'alt' => $alt ) ); if ( $forceKnown ) { - $link .= $sk->linkKnown( $rt, htmlspecialchars( $rt->getFullText() ) ); + $link .= Linker::linkKnown( $rt, htmlspecialchars( $rt->getFullText(), array(), array( 'redirect' => 'no' ) ) ); } else { - $link .= $sk->link( $rt, htmlspecialchars( $rt->getFullText() ) ); + $link .= Linker::link( $rt, htmlspecialchars( $rt->getFullText() ), array(), array( 'redirect' => 'no' ) ); } } - $imageUrl = $wgStylePath . '/common/images/redirect' . $imageDir . '.png'; + $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>'; @@ -1583,12 +1103,12 @@ class Article { * Builds trackback links for article display if $wgUseTrackbacks is set to true */ public function addTrackbacks() { - global $wgOut, $wgUser; + global $wgOut; $dbr = wfGetDB( DB_SLAVE ); $tbs = $dbr->select( 'trackbacks', array( 'tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name' ), - array( 'tb_page' => $this->getID() ) + array( 'tb_page' => $this->mPage->getID() ) ); if ( !$dbr->numRows( $tbs ) ) { @@ -1601,9 +1121,9 @@ class Article { foreach ( $tbs as $o ) { $rmvtxt = ""; - if ( $wgUser->isAllowed( 'trackback' ) ) { - $delurl = $this->mTitle->getFullURL( "action=deletetrackback&tbid=" . - $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) ); + if ( $this->getContext()->getUser()->isAllowed( 'trackback' ) ) { + $delurl = $this->getTitle()->getFullURL( "action=deletetrackback&tbid=" . + $o->tb_id . "&token=" . urlencode( $this->getContext()->getUser()->editToken() ) ); $rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) ); } @@ -1621,29 +1141,10 @@ class Article { /** * Removes trackback record for current article from trackbacks table + * @deprecated since 1.18 */ public function deletetrackback() { - global $wgUser, $wgRequest, $wgOut; - - if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) { - $wgOut->addWikiMsg( 'sessionfailure' ); - - return; - } - - $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); - - if ( count( $permission_errors ) ) { - $wgOut->showPermissionsErrorPage( $permission_errors ); - - return; - } - - $db = wfGetDB( DB_MASTER ); - $db->delete( 'trackbacks', array( 'tb_id' => $wgRequest->getInt( 'tbid' ) ) ); - - $wgOut->addWikiMsg( 'trackbackdeleteok' ); - $this->mTitle->invalidateCache(); + return Action::factory( 'deletetrackback', $this )->show(); } /** @@ -1661,766 +1162,24 @@ class Article { * Handle action=purge */ public function purge() { - global $wgUser, $wgRequest, $wgOut; - - if ( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) { - //FIXME: shouldn't this be in doPurge()? - if ( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { - $this->doPurge(); - $this->view(); - } - } else { - $formParams = array( - 'method' => 'post', - 'action' => $wgRequest->getRequestURL(), - ); - - $wgOut->addWikiMsg( 'confirm-purge-top' ); - - $form = Html::openElement( 'form', $formParams ); - $form .= Xml::submitButton( wfMsg( 'confirm_purge_button' ) ); - $form .= Html::closeElement( 'form' ); - - $wgOut->addHTML( $form ); - $wgOut->addWikiMsg( 'confirm-purge-bottom' ); - - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - } - } - - /** - * Perform the actions of a page purging - */ - public function doPurge() { - global $wgUseSquid; - - // Invalidate the cache - $this->mTitle->invalidateCache(); - - if ( $wgUseSquid ) { - // Commit the transaction before the purge is sent - $dbw = wfGetDB( DB_MASTER ); - $dbw->commit(); - - // Send purge - $update = SquidUpdate::newSimplePurge( $this->mTitle ); - $update->doUpdate(); - } - - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - global $wgMessageCache; - - if ( $this->getID() == 0 ) { - $text = false; - } else { - $text = $this->getRawText(); - } - - $wgMessageCache->replace( $this->mTitle->getDBkey(), $text ); - } - } - - /** - * 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 $dbw Database - * @return int The newly created page_id key, or false if the title already existed - * @private - */ - 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->mTitle->resetArticleId( $newid ); - } - wfProfileOut( __METHOD__ ); - - return $affected ? $newid : false; - } - - /** - * Update the page record to point to a newly saved revision. - * - * @param $dbw DatabaseBase: object - * @param $revision Revision: For ID number, and text used to set - length and redirect status fields - * @param $lastRevision Integer: 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 $lastRevIsRedirect Boolean: if given, will optimize adding and - * removing rows in redirect table. - * @return bool true on success, false on failure - * @private - */ - public function updateRevisionOn( &$dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { - wfProfileIn( __METHOD__ ); - - $text = $revision->getText(); - $rt = Title::newFromRedirectRecurse( $text ); - - $conditions = array( 'page_id' => $this->getId() ); - - if ( !is_null( $lastRevision ) ) { - # An extra check against threads stepping on each other - $conditions['page_latest'] = $lastRevision; - } - - $dbw->update( 'page', - array( /* SET */ - 'page_latest' => $revision->getId(), - 'page_touched' => $dbw->timestamp(), - 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, - 'page_is_redirect' => $rt !== null ? 1 : 0, - 'page_len' => strlen( $text ), - ), - $conditions, - __METHOD__ ); - - $result = $dbw->affectedRows() != 0; - if ( $result ) { - $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); - } - - wfProfileOut( __METHOD__ ); - return $result; - } - - /** - * Add row to the redirect table if this is a redirect, remove otherwise. - * - * @param $dbw Database - * @param $redirectTitle a title object pointing to the redirect target, - * or NULL if this is not a redirect - * @param $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 || is_null( $lastRevIsRedirect ) || $lastRevIsRedirect !== $isRedirect ) { - 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 ); - } - - return true; - } - - /** - * If the given revision is newer than the currently set page_latest, - * update the page record. Otherwise, do nothing. - * - * @param $dbw Database object - * @param $revision Revision object - * @return mixed - */ - 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; - } - - /** - * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...) - * @param $text String: new text of the section - * @param $summary String: new section's subject, only if $section is 'new' - * @param $edittime String: revision timestamp or null to use the current revision - * @return string Complete article text, or null if error - */ - public function replaceSection( $section, $text, $summary = '', $edittime = null ) { - wfProfileIn( __METHOD__ ); - - if ( strval( $section ) == '' ) { - // Whole-page edit; let the whole text through - } else { - if ( is_null( $edittime ) ) { - $rev = Revision::newFromTitle( $this->mTitle ); - } else { - $dbw = wfGetDB( DB_MASTER ); - $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); - } - - if ( !$rev ) { - wfDebug( "Article::replaceSection asked for bogus section (page: " . - $this->getId() . "; section: $section; edittime: $edittime)\n" ); - wfProfileOut( __METHOD__ ); - return null; - } - - $oldtext = $rev->getText(); - - if ( $section == 'new' ) { - # Inserting a new section - $subject = $summary ? wfMsgForContent( 'newsectionheaderdefaultlevel', $summary ) . "\n\n" : ''; - $text = strlen( trim( $oldtext ) ) > 0 - ? "{$oldtext}\n\n{$subject}{$text}" - : "{$subject}{$text}"; - } else { - # Replacing an existing section; roll out the big guns - global $wgParser; - - $text = $wgParser->replaceSection( $oldtext, $section, $text ); - } - } - - wfProfileOut( __METHOD__ ); - return $text; - } - - /** - * This function is not deprecated until somebody fixes the core not to use - * it. Nevertheless, use Article::doEdit() instead. - */ - function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC = false, $comment = false, $bot = false ) { - $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | - ( $isminor ? EDIT_MINOR : 0 ) | - ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) | - ( $bot ? EDIT_FORCE_BOT : 0 ); - - # If this is a comment, add the summary as headline - if ( $comment && $summary != "" ) { - $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $summary ) . "\n\n" . $text; - } - $this->doEdit( $text, $summary, $flags ); - - $dbw = wfGetDB( DB_MASTER ); - if ( $watchthis ) { - if ( !$this->mTitle->userIsWatching() ) { - $dbw->begin(); - $this->doWatch(); - $dbw->commit(); - } - } else { - if ( $this->mTitle->userIsWatching() ) { - $dbw->begin(); - $this->doUnwatch(); - $dbw->commit(); - } - } - $this->doRedirect( $this->isRedirect( $text ) ); - } - - /** - * @deprecated use Article::doEdit() - */ - function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) { - $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | - ( $minor ? EDIT_MINOR : 0 ) | - ( $forceBot ? EDIT_FORCE_BOT : 0 ); - - $status = $this->doEdit( $text, $summary, $flags ); - - if ( !$status->isOK() ) { - return false; - } - - $dbw = wfGetDB( DB_MASTER ); - if ( $watchthis ) { - if ( !$this->mTitle->userIsWatching() ) { - $dbw->begin(); - $this->doWatch(); - $dbw->commit(); - } - } else { - if ( $this->mTitle->userIsWatching() ) { - $dbw->begin(); - $this->doUnwatch(); - $dbw->commit(); - } - } - - $extraQuery = ''; // Give extensions a chance to modify URL query on update - wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) ); - - $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery ); - return true; - } - - /** - * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed. - * @param $flags Int - * @return Int updated $flags - */ - function checkFlags( $flags ) { - if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) { - if ( $this->mTitle->getArticleID() ) { - $flags |= EDIT_UPDATE; - } else { - $flags |= EDIT_NEW; - } - } - - return $flags; - } - - /** - * Article::doEdit() - * - * Change an existing article or create a new article. Updates RC and all necessary caches, - * optionally via the deferred update array. - * - * $wgUser must be set before calling this function. - * - * @param $text String: new text - * @param $summary String: edit summary - * @param $flags Integer 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 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 $baseRevId the revision ID this edit was based off, if any - * @param $user Optional user object, $wgUser will be used if not passed - * - * @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 - */ - public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { - global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries; - - # Low-level sanity check - if ( $this->mTitle->getText() === '' ) { - throw new MWException( 'Something is trying to edit an article with an empty title' ); - } - - wfProfileIn( __METHOD__ ); - - $user = is_null( $user ) ? $wgUser : $user; - $status = Status::newGood( array() ); - - # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already - $this->loadPageData(); - - $flags = $this->checkFlags( $flags ); - - if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary, - $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) ) - { - wfDebug( __METHOD__ . ": ArticleSave 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; - - $oldtext = $this->getRawText(); // current revision - $oldsize = strlen( $oldtext ); - - # Provide autosummaries if one is not provided and autosummaries are enabled. - if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { - $summary = $this->getAutosummary( $oldtext, $text, $flags ); - } - - $editInfo = $this->prepareTextForEdit( $text ); - $text = $editInfo->pst; - $newsize = strlen( $text ); - - $dbw = wfGetDB( DB_MASTER ); - $now = wfTimestampNow(); - $this->mTimestamp = $now; - - if ( $flags & EDIT_UPDATE ) { - # Update article, but only if changed. - $status->value['new'] = false; - - # Make sure the revision is either completely inserted or not inserted at all - if ( !$wgDBtransactions ) { - $userAbort = ignore_user_abort( true ); - } - - $changed = ( strcmp( $text, $oldtext ) != 0 ); - - if ( $changed ) { - $this->mGoodAdjustment = (int)$this->isCountable( $text ) - - (int)$this->isCountable( $oldtext ); - $this->mTotalAdjustment = 0; - - if ( !$this->mLatest ) { - # Article gone missing - wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); - $status->fatal( 'edit-gone-missing' ); - - wfProfileOut( __METHOD__ ); - return $status; - } - - $revision = new Revision( array( - 'page' => $this->getId(), - 'comment' => $summary, - 'minor_edit' => $isminor, - 'text' => $text, - 'parent_id' => $this->mLatest, - 'user' => $user->getId(), - 'user_text' => $user->getName(), - ) ); - - $dbw->begin(); - $revisionId = $revision->insertOn( $dbw ); - - # Update page - # - # Note that we use $this->mLatest instead of fetching a value from the master DB - # during the course of this function. This makes sure that EditPage can detect - # edit conflicts reliably, either by $ok here, or by $article->getTimestamp() - # before this function is called. A previous function used a separate query, this - # creates a window where concurrent edits can cause an ignored edit conflict. - $ok = $this->updateRevisionOn( $dbw, $revision, $this->mLatest ); - - if ( !$ok ) { - /* Belated edit conflict! Run away!! */ - $status->fatal( 'edit-conflict' ); - - # Delete the invalid revision if the DB is not transactional - if ( !$wgDBtransactions ) { - $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ ); - } - - $revisionId = 0; - $dbw->rollback(); - } else { - global $wgUseRCPatrol; - 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 && $this->mTitle->userCan( 'autopatrol' ); - # Add RC row to the DB - $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, - $this->mLatest, $this->getTimestamp(), $bot, '', $oldsize, $newsize, - $revisionId, $patrolled - ); - - # Log auto-patrolled edits - if ( $patrolled ) { - PatrolLog::record( $rc, true ); - } - } - $user->incEditCount(); - $dbw->commit(); - } - } else { - $status->warning( 'edit-no-change' ); - $revision = null; - // Keep the same revision ID, but do some updates on it - $revisionId = $this->getRevIdFetched(); - // Update page_touched, this is usually implicit in the page update - // Other cache updates are done in onArticleEdit() - $this->mTitle->invalidateCache(); - } - - if ( !$wgDBtransactions ) { - ignore_user_abort( $userAbort ); - } - - // Now that ignore_user_abort is restored, we can respond to fatal errors - if ( !$status->isOK() ) { - wfProfileOut( __METHOD__ ); - return $status; - } - - # Invalidate cache of this article and all pages using this article - # as a template. Partly deferred. - Article::onArticleEdit( $this->mTitle ); - # Update links tables, site stats, etc. - $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); - } else { - # Create new article - $status->value['new'] = true; - - # Set statistics members - # We work out if it's countable after PST to avoid counter drift - # when articles are created with {{subst:}} - $this->mGoodAdjustment = (int)$this->isCountable( $text ); - $this->mTotalAdjustment = 1; - - $dbw->begin(); - - # 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(); - $status->fatal( 'edit-already-exists' ); - - wfProfileOut( __METHOD__ ); - return $status; - } - - # Save the revision text... - $revision = new Revision( array( - 'page' => $newid, - 'comment' => $summary, - 'minor_edit' => $isminor, - 'text' => $text, - 'user' => $user->getId(), - 'user_text' => $user->getName(), - ) ); - $revisionId = $revision->insertOn( $dbw ); - - $this->mTitle->resetArticleID( $newid ); - - # 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 ) ) { - global $wgUseRCPatrol, $wgUseNPPatrol; - - # Mark as patrolled if the user can do so - $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && $this->mTitle->userCan( 'autopatrol' ); - # Add RC row to the DB - $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, - '', strlen( $text ), $revisionId, $patrolled ); - - # Log auto-patrolled edits - if ( $patrolled ) { - PatrolLog::record( $rc, true ); - } - } - $user->incEditCount(); - $dbw->commit(); - - # Update links, etc. - $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, true ); - - # Clear caches - Article::onArticleCreate( $this->mTitle ); - - wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); - } - - # Do updates right now unless deferral was requested - if ( !( $flags & EDIT_DEFER_UPDATES ) ) { - wfDoUpdates(); - } - - // Return the new revision (or null) to the caller - $status->value['revision'] = $revision; - - wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) ); - - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @deprecated wrapper for doRedirect - */ - public function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) { - wfDeprecated( __METHOD__ ); - $this->doRedirect( $this->isRedirect( $text ), $sectionanchor ); - } - - /** - * Output a redirect back to the article. - * This is typically used after an edit. - * - * @param $noRedir Boolean: add redirect=no - * @param $sectionAnchor String: section to redirect to, including "#" - * @param $extraQuery String: extra query params - */ - public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) { - global $wgOut; - - if ( $noRedir ) { - $query = 'redirect=no'; - if ( $extraQuery ) - $query .= "&$extraQuery"; - } else { - $query = $extraQuery; - } - - $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor ); + return Action::factory( 'purge', $this )->show(); } /** * Mark this particular edit/page as patrolled + * @deprecated since 1.18 */ public function markpatrolled() { - global $wgOut, $wgUser, $wgRequest; - - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - - # If we haven't been given an rc_id value, we can't do anything - $rcid = (int) $wgRequest->getVal( 'rcid' ); - - if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ), $rcid ) ) { - $wgOut->showErrorPage( 'sessionfailure-title', 'sessionfailure' ); - return; - } - - $rc = RecentChange::newFromId( $rcid ); - - if ( is_null( $rc ) ) { - $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); - return; - } - - # It would be nice to see where the user had actually come from, but for now just guess - $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges'; - $return = SpecialPage::getTitleFor( $returnto ); - - $errors = $rc->doMarkPatrolled(); - - if ( in_array( array( 'rcpatroldisabled' ), $errors ) ) { - $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); - - return; - } - - if ( in_array( array( 'hookaborted' ), $errors ) ) { - // The hook itself has handled any output - return; - } - - if ( in_array( array( 'markedaspatrollederror-noautopatrol' ), $errors ) ) { - $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) ); - $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' ); - $wgOut->returnToMain( false, $return ); - - return; - } - - if ( !empty( $errors ) ) { - $wgOut->showPermissionsErrorPage( $errors ); - - return; - } - - # Inform the user - $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) ); - $wgOut->addWikiMsg( 'markedaspatrolledtext', $rc->getTitle()->getPrefixedText() ); - $wgOut->returnToMain( false, $return ); + Action::factory( 'markpatrolled', $this )->show(); } /** - * User-interface handler for the "watch" action + * User-interface handler for the "watch" action. + * Requires Request to pass a token as of 1.18. + * @deprecated since 1.18 */ public function watch() { - global $wgUser, $wgOut; - - if ( $wgUser->isAnon() ) { - $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); - return; - } - - if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; - } - - if ( $this->doWatch() ) { - $wgOut->setPagetitle( wfMsg( 'addedwatch' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() ); - } - - $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); + Action::factory( 'watch', $this )->show(); } /** @@ -2429,64 +1188,30 @@ class Article { * This is safe to be called multiple times * * @return bool true on successful watch operation + * @deprecated since 1.18 */ public function doWatch() { global $wgUser; - - if ( $wgUser->isAnon() ) { - return false; - } - - if ( wfRunHooks( 'WatchArticle', array( &$wgUser, &$this ) ) ) { - $wgUser->addWatch( $this->mTitle ); - return wfRunHooks( 'WatchArticleComplete', array( &$wgUser, &$this ) ); - } - - return false; + return WatchAction::doWatch( $this->getTitle(), $wgUser ); } /** * User interface handler for the "unwatch" action. + * Requires Request to pass a token as of 1.18. + * @deprecated since 1.18 */ public function unwatch() { - global $wgUser, $wgOut; - - if ( $wgUser->isAnon() ) { - $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); - return; - } - - if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; - } - - if ( $this->doUnwatch() ) { - $wgOut->setPagetitle( wfMsg( 'removedwatch' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() ); - } - - $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); + Action::factory( 'unwatch', $this )->show(); } /** * Stop watching a page * @return bool true on successful unwatch + * @deprecated since 1.18 */ public function doUnwatch() { global $wgUser; - - if ( $wgUser->isAnon() ) { - return false; - } - - if ( wfRunHooks( 'UnwatchArticle', array( &$wgUser, &$this ) ) ) { - $wgUser->removeWatch( $this->mTitle ); - return wfRunHooks( 'UnwatchArticleComplete', array( &$wgUser, &$this ) ); - } - - return false; + return WatchAction::doUnwatch( $this->getTitle(), $wgUser ); } /** @@ -2505,316 +1230,60 @@ class Article { } /** - * Update the article's restriction field, and leave a log entry. - * - * @param $limit Array: set of restriction keys - * @param $reason String - * @param &$cascade Integer. Set to false if cascading protection isn't allowed. - * @param $expiry Array: per restriction type expiration - * @return bool true on success + * Info about this page + * Called for ?action=info when $wgAllowPageInfo is on. */ - public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) { - global $wgUser, $wgContLang; - - $restrictionTypes = $this->mTitle->getRestrictionTypes(); - - $id = $this->mTitle->getArticleID(); - - if ( $id <= 0 ) { - wfDebug( "updateRestrictions failed: article id $id <= 0\n" ); - return false; - } - - if ( wfReadOnly() ) { - wfDebug( "updateRestrictions failed: read-only\n" ); - return false; - } - - if ( !$this->mTitle->userCan( 'protect' ) ) { - wfDebug( "updateRestrictions failed: insufficient permissions\n" ); - return false; - } - - if ( !$cascade ) { - $cascade = false; - } - - // Take this opportunity to purge out expired restrictions - Title::purgeExpiredRestrictions(); - - # FIXME: Same limitations as described in ProtectionForm.php (line 37); - # we expect a single selection, but the schema allows otherwise. - $current = array(); - $updated = Article::flattenRestrictions( $limit ); - $changed = false; - - foreach ( $restrictionTypes as $action ) { - if ( isset( $expiry[$action] ) ) { - # Get current restrictions on $action - $aLimits = $this->mTitle->getRestrictions( $action ); - $current[$action] = implode( '', $aLimits ); - # Are any actual restrictions being dealt with here? - $aRChanged = count( $aLimits ) || !empty( $limit[$action] ); - - # If something changed, we need to log it. Checking $aRChanged - # assures that "unprotecting" a page that is not protected does - # not log just because the expiry was "changed". - if ( $aRChanged && $this->mTitle->mRestrictionsExpiry[$action] != $expiry[$action] ) { - $changed = true; - } - } - } - - $current = Article::flattenRestrictions( $current ); - - $changed = ( $changed || $current != $updated ); - $changed = $changed || ( $updated && $this->mTitle->areRestrictionsCascading() != $cascade ); - $protect = ( $updated != '' ); - - # If nothing's changed, do nothing - if ( $changed ) { - if ( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) { - $dbw = wfGetDB( DB_MASTER ); - - # Prepare a null revision to be added to the history - $modified = $current != '' && $protect; - - if ( $protect ) { - $comment_type = $modified ? 'modifiedarticleprotection' : 'protectedarticle'; - } else { - $comment_type = 'unprotectedarticle'; - } - - $comment = $wgContLang->ucfirst( wfMsgForContent( $comment_type, $this->mTitle->getPrefixedText() ) ); - - # Only restrictions with the 'protect' right can cascade... - # Otherwise, people who cannot normally protect can "protect" pages via transclusion - $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' ); - - # The schema allows multiple restrictions - if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) { - $cascade = false; - } - - $cascade_description = ''; - - if ( $cascade ) { - $cascade_description = ' [' . wfMsgForContent( 'protect-summary-cascade' ) . ']'; - } - - if ( $reason ) { - $comment .= ": $reason"; - } - - $editComment = $comment; - $encodedExpiry = array(); - $protect_description = ''; - foreach ( $limit as $action => $restrictions ) { - if ( !isset( $expiry[$action] ) ) - $expiry[$action] = Block::infinity(); - - $encodedExpiry[$action] = Block::encodeExpiry( $expiry[$action], $dbw ); - if ( $restrictions != '' ) { - $protect_description .= "[$action=$restrictions] ("; - if ( $encodedExpiry[$action] != 'infinity' ) { - $protect_description .= wfMsgForContent( 'protect-expiring', - $wgContLang->timeanddate( $expiry[$action], false, false ) , - $wgContLang->date( $expiry[$action], false, false ) , - $wgContLang->time( $expiry[$action], false, false ) ); - } else { - $protect_description .= wfMsgForContent( 'protect-expiry-indefinite' ); - } - - $protect_description .= ') '; - } - } - $protect_description = trim( $protect_description ); - - if ( $protect_description && $protect ) { - $editComment .= " ($protect_description)"; - } - - if ( $cascade ) { - $editComment .= "$cascade_description"; - } - - # Update restrictions table - foreach ( $limit as $action => $restrictions ) { - if ( $restrictions != '' ) { - $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ), - array( 'pr_page' => $id, - 'pr_type' => $action, - 'pr_level' => $restrictions, - 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0, - 'pr_expiry' => $encodedExpiry[$action] - ), - __METHOD__ - ); - } else { - $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, - 'pr_type' => $action ), __METHOD__ ); - } - } - - # Insert a null revision - $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true ); - $nullRevId = $nullRevision->insertOn( $dbw ); - - $latest = $this->getLatest(); - # Update page record - $dbw->update( 'page', - array( /* SET */ - 'page_touched' => $dbw->timestamp(), - 'page_restrictions' => '', - 'page_latest' => $nullRevId - ), array( /* WHERE */ - 'page_id' => $id - ), 'Article::protect' - ); - - wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $wgUser ) ); - wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) ); - - # Update the protection log - $log = new LogPage( 'protect' ); - if ( $protect ) { - $params = array( $protect_description, $cascade ? 'cascade' : '' ); - $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason ), $params ); - } else { - $log->addEntry( 'unprotect', $this->mTitle, $reason ); - } - } # End hook - } # End "changed" check - - return true; + public function info() { + Action::factory( 'info', $this )->show(); } /** - * Take an array of page restrictions and flatten it to a string - * suitable for insertion into the page_restrictions field. - * @param $limit Array - * @return String + * Overriden by ImagePage class, only present here to avoid a fatal error + * Called for ?action=revert */ - protected static function flattenRestrictions( $limit ) { - if ( !is_array( $limit ) ) { - throw new MWException( 'Article::flattenRestrictions given non-array restriction set' ); - } - - $bits = array(); - ksort( $limit ); - - foreach ( $limit as $action => $restrictions ) { - if ( $restrictions != '' ) { - $bits[] = "$action=$restrictions"; - } - } + public function revert() { + Action::factory( 'revert', $this )->show(); + } - return implode( ':', $bits ); + /** + * User interface for rollback operations + */ + public function rollback() { + Action::factory( 'rollback', $this )->show(); } /** - * Auto-generates a deletion reason + * Output a redirect back to the article. + * This is typically used after an edit. * - * @param &$hasHistory Boolean: whether the page has a history - * @return mixed String containing deletion reason or empty string, or boolean false - * if no revision occurred + * @deprecated in 1.18; call $wgOut->redirect() directly + * @param $noRedir Boolean: add redirect=no + * @param $sectionAnchor String: section to redirect to, including "#" + * @param $extraQuery String: extra query params */ - public function generateReason( &$hasHistory ) { - global $wgContLang; - - $dbw = wfGetDB( DB_MASTER ); - // Get the last revision - $rev = Revision::newFromTitle( $this->mTitle ); - - if ( is_null( $rev ) ) { - return false; - } - - // Get the article's contents - $contents = $rev->getText(); - $blank = false; - - // If the page is blank, use the text from the previous revision, - // which can only be blank if there's a move/import/protect dummy revision involved - if ( $contents == '' ) { - $prev = $rev->getPrevious(); - - if ( $prev ) { - $contents = $prev->getText(); - $blank = true; - } - } - - // Find out if there was only one contributor - // Only scan the last 20 revisions - $res = $dbw->select( 'revision', 'rev_user_text', - array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ), - __METHOD__, - array( 'LIMIT' => 20 ) - ); - - if ( $res === false ) { - // This page has no revisions, which is very weird - return false; - } - - $hasHistory = ( $res->numRows() > 1 ); - $row = $dbw->fetchObject( $res ); - - if ( $row ) { // $row is false if the only contributor is hidden - $onlyAuthor = $row->rev_user_text; - // Try to find a second contributor - foreach ( $res as $row ) { - if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 - $onlyAuthor = false; - break; - } - } - } else { - $onlyAuthor = false; - } + public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) { + wfDeprecated( __METHOD__ ); + global $wgOut; - // Generate the summary with a '$1' placeholder - if ( $blank ) { - // The current revision is blank and the one before is also - // blank. It's just not our lucky day - $reason = wfMsgForContent( 'exbeforeblank', '$1' ); + if ( $noRedir ) { + $query = 'redirect=no'; + if ( $extraQuery ) + $query .= "&$extraQuery"; } else { - if ( $onlyAuthor ) { - $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor ); - } else { - $reason = wfMsgForContent( 'excontent', '$1' ); - } - } - - if ( $reason == '-' ) { - // Allow these UI messages to be blanked out cleanly - return ''; + $query = $extraQuery; } - // Replace newlines with spaces to prevent uglyness - $contents = preg_replace( "/[\n\r]/", ' ', $contents ); - // Calculate the maximum amount of chars to get - // Max content length = max comment length - length of the comment (excl. $1) - '...' - $maxLength = 255 - ( strlen( $reason ) - 2 ) - 3; - $contents = $wgContLang->truncate( $contents, $maxLength ); - // Remove possible unfinished links - $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents ); - // Now replace the '$1' placeholder - $reason = str_replace( '$1', $contents, $reason ); - - return $reason; + $wgOut->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor ); } - - /* + /** * UI entry point for page deletion */ public function delete() { - global $wgUser, $wgOut, $wgRequest; + global $wgOut, $wgRequest; $confirm = $wgRequest->wasPosted() && - $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); + $this->getContext()->getUser()->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' ); $this->DeleteReason = $wgRequest->getText( 'wpReason' ); @@ -2829,7 +1298,7 @@ class Article { } # Flag to hide all contents of the archived revisions - $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' ); + $suppress = $wgRequest->getVal( 'wpSuppress' ) && $this->getContext()->getUser()->isAllowed( 'suppressrevision' ); # This code desperately needs to be totally rewritten @@ -2841,7 +1310,7 @@ class Article { } # Check permissions - $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); + $permission_errors = $this->getTitle()->getUserPermissionsErrors( 'delete', $this->getContext()->getUser() ); if ( count( $permission_errors ) > 0 ) { $wgOut->showPermissionsErrorPage( $permission_errors ); @@ -2849,33 +1318,34 @@ class Article { return; } - $wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->mTitle->getPrefixedText() ) ); + $wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->getTitle()->getPrefixedText() ) ); # Better double-check that it hasn't been deleted yet! $dbw = wfGetDB( DB_MASTER ); - $conds = $this->mTitle->pageCond(); + $conds = $this->getTitle()->pageCond(); $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); if ( $latest === false ) { $wgOut->showFatalError( Html::rawElement( 'div', array( 'class' => 'error mw-error-cannotdelete' ), - wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() ) + wfMsgExt( 'cannotdelete', array( 'parse' ), + wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ) ) ); $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); LogEventsList::showLogExtract( $wgOut, 'delete', - $this->mTitle->getPrefixedText() + $this->getTitle()->getPrefixedText() ); return; } # Hack for big sites - $bigHistory = $this->isBigDeletion(); - if ( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) { + $bigHistory = $this->mPage->isBigDeletion(); + if ( $bigHistory && !$this->getTitle()->userCan( 'bigdelete' ) ) { global $wgLang, $wgDeleteRevisionsLimit; $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", @@ -2887,9 +1357,9 @@ class Article { if ( $confirm ) { $this->doDelete( $reason, $suppress ); - if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) { + if ( $wgRequest->getCheck( 'wpWatch' ) && $this->getContext()->getUser()->isLoggedIn() ) { $this->doWatch(); - } elseif ( $this->mTitle->userIsWatching() ) { + } elseif ( $this->getTitle()->userIsWatching() ) { $this->doUnwatch(); } @@ -2906,12 +1376,14 @@ class Article { if ( $hasHistory && !$confirm ) { global $wgLang; - $skin = $wgUser->getSkin(); - $revisions = $this->estimateRevisionCount(); - //FIXME: lego + $revisions = $this->mPage->estimateRevisionCount(); + // @todo FIXME: i18n issue/patchwork message $wgOut->addHTML( '<strong class="mw-delete-warning-revisions">' . wfMsgExt( 'historywarning', array( 'parseinline' ), $wgLang->formatNum( $revisions ) ) . - wfMsgHtml( 'word-separator' ) . $skin->historyLink() . + wfMsgHtml( 'word-separator' ) . Linker::link( $this->getTitle(), + wfMsgHtml( 'history' ), + array( 'rel' => 'archives' ), + array( 'action' => 'history' ) ) . '</strong>' ); @@ -2926,103 +1398,24 @@ class Article { } /** - * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions - */ - public function isBigDeletion() { - global $wgDeleteRevisionsLimit; - - if ( $wgDeleteRevisionsLimit ) { - $revCount = $this->estimateRevisionCount(); - - return $revCount > $wgDeleteRevisionsLimit; - } - - return false; - } - - /** - * @return int approximate revision count - */ - public function estimateRevisionCount() { - $dbr = wfGetDB( DB_SLAVE ); - - // For an exact count... - // return $dbr->selectField( 'revision', 'COUNT(*)', - // array( 'rev_page' => $this->getId() ), __METHOD__ ); - return $dbr->estimateRowCount( 'revision', '*', - array( 'rev_page' => $this->getId() ), __METHOD__ ); - } - - /** - * Get the last N authors - * @param $num Integer: number of revisions to get - * @param $revLatest String: 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__, $this->getSelectOptions( 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; - } - - /** * Output deletion confirmation dialog - * FIXME: Move to another file? + * @todo FIXME: Move to another file? * @param $reason String: prefilled reason */ public function confirmDelete( $reason ) { - global $wgOut, $wgUser; + global $wgOut; wfDebug( "Article::confirmDelete\n" ); - $deleteBackLink = $wgUser->getSkin()->linkKnown( $this->mTitle ); + $deleteBackLink = Linker::linkKnown( $this->getTitle() ); $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $deleteBackLink ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addWikiMsg( 'confirmdeletetext' ); wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) ); - if ( $wgUser->isAllowed( 'suppressrevision' ) ) { - $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"> + if ( $this->getContext()->getUser()->isAllowed( 'suppressrevision' ) ) { + $suppress = "<tr id=\"wpDeleteSuppressRow\"> <td></td> <td class='mw-input'><strong>" . Xml::checkLabel( wfMsg( 'revdelete-suppress' ), @@ -3032,10 +1425,10 @@ class Article { } else { $suppress = ''; } - $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(); + $checkWatch = $this->getContext()->getUser()->getBoolOption( 'watchdeletion' ) || $this->getTitle()->userIsWatching(); $form = Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $this->mTitle->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . + 'action' => $this->getTitle()->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) . Xml::tags( 'legend', null, wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) ) . Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) . @@ -3065,7 +1458,7 @@ class Article { </tr>"; # Disallow watching if user is not logged in - if ( $wgUser->isLoggedIn() ) { + if ( $this->getContext()->getUser()->isLoggedIn() ) { $form .= " <tr> <td></td> @@ -3087,13 +1480,12 @@ class Article { </tr>" . Xml::closeElement( 'table' ) . Xml::closeElement( 'fieldset' ) . - Html::hidden( 'wpEditToken', $wgUser->editToken() ) . + Html::hidden( 'wpEditToken', $this->getContext()->getUser()->editToken() ) . Xml::closeElement( 'form' ); - if ( $wgUser->isAllowed( 'editinterface' ) ) { - $skin = $wgUser->getSkin(); + if ( $this->getContext()->getUser()->isAllowed( 'editinterface' ) ) { $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ); - $link = $skin->link( + $link = Linker::link( $title, wfMsgHtml( 'delete-edit-reasonlist' ), array(), @@ -3105,7 +1497,7 @@ class Article { $wgOut->addHTML( $form ); $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); LogEventsList::showLogExtract( $wgOut, 'delete', - $this->mTitle->getPrefixedText() + $this->getTitle()->getPrefixedText() ); } @@ -3113,31 +1505,29 @@ class Article { * Perform a deletion and output success or failure messages */ public function doDelete( $reason, $suppress = false ) { - global $wgOut, $wgUser; + global $wgOut; - $id = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE ); + $id = $this->getTitle()->getArticleID( Title::GAID_FOR_UPDATE ); $error = ''; - if ( wfRunHooks( 'ArticleDelete', array( &$this, &$wgUser, &$reason, &$error ) ) ) { - if ( $this->doDeleteArticle( $reason, $suppress, $id ) ) { - $deleted = $this->mTitle->getPrefixedText(); + if ( $this->mPage->doDeleteArticle( $reason, $suppress, $id, $error ) ) { + $deleted = $this->getTitle()->getPrefixedText(); - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'; + $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'; - $wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink ); - $wgOut->returnToMain( false ); - wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$wgUser, $reason, $id ) ); - } + $wgOut->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink ); + $wgOut->returnToMain( false ); } else { if ( $error == '' ) { $wgOut->showFatalError( Html::rawElement( 'div', array( 'class' => 'error mw-error-cannotdelete' ), - wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() ) + wfMsgExt( 'cannotdelete', array( 'parse' ), + wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ) ) ); @@ -3146,7 +1536,7 @@ class Article { LogEventsList::showLogExtract( $wgOut, 'delete', - $this->mTitle->getPrefixedText() + $this->getTitle()->getPrefixedText() ); } else { $wgOut->showFatalError( $error ); @@ -3155,574 +1545,6 @@ class Article { } /** - * Back-end article deletion - * Deletes the article with database consistency, writes logs, purges caches - * - * @param $reason string delete reason for deletion log - * @param suppress bitfield - * Revision::DELETED_TEXT - * Revision::DELETED_COMMENT - * Revision::DELETED_USER - * Revision::DELETED_RESTRICTED - * @param $id int article ID - * @param $commit boolean defaults to true, triggers transaction end - * @return boolean true if successful - */ - public function doDeleteArticle( $reason, $suppress = false, $id = 0, $commit = true ) { - global $wgDeferredUpdateList, $wgUseTrackbacks; - - wfDebug( __METHOD__ . "\n" ); - - $dbw = wfGetDB( DB_MASTER ); - $t = $this->mTitle->getDBkey(); - $id = $id ? $id : $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE ); - - if ( $t === '' || $id == 0 ) { - return false; - } - - $u = new SiteStatsUpdate( 0, 1, - (int)$this->isCountable( $this->getRawText() ), -1 ); - array_push( $wgDeferredUpdateList, $u ); - - // 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'; - } - - $dbw->begin(); - // 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. - $dbw->insertSelect( 'archive', array( 'page', 'revision' ), - 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_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 - ), array( - 'page_id' => $id, - 'page_id = rev_page' - ), __METHOD__ - ); - - # Delete restrictions for it - $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); - - # Now that it's safely backed up, delete it - $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); - $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy - - if ( !$ok ) { - $dbw->rollback(); - return false; - } - - # Fix category table counts - $cats = array(); - $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ ); - - foreach ( $res as $row ) { - $cats [] = $row->cl_to; - } - - $this->updateCategoryCounts( array(), $cats ); - - # If using cascading deletes, we can skip some explicit deletes - if ( !$dbw->cascadingDeletes() ) { - $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); - - if ( $wgUseTrackbacks ) - $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ ); - - # Delete outgoing links - $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) ); - $dbw->delete( 'imagelinks', array( 'il_from' => $id ) ); - $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) ); - $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) ); - $dbw->delete( 'externallinks', array( 'el_from' => $id ) ); - $dbw->delete( 'langlinks', array( 'll_from' => $id ) ); - $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ) ); - $dbw->delete( 'redirect', array( 'rd_from' => $id ) ); - } - - # If using cleanup triggers, we can skip some manual deletes - if ( !$dbw->cleanupTriggers() ) { - # Clean up recentchanges entries... - $dbw->delete( 'recentchanges', - array( 'rc_type != ' . RC_LOG, - 'rc_namespace' => $this->mTitle->getNamespace(), - 'rc_title' => $this->mTitle->getDBkey() ), - __METHOD__ ); - $dbw->delete( 'recentchanges', - array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ), - __METHOD__ ); - } - - # Clear caches - Article::onArticleDelete( $this->mTitle ); - - # Clear the cached article id so the interface doesn't act like we exist - $this->mTitle->resetArticleID( 0 ); - - # Log the deletion, if the page was suppressed, log it at Oversight instead - $logtype = $suppress ? 'suppress' : 'delete'; - $log = new LogPage( $logtype ); - - # Make sure logging got through - $log->addEntry( 'delete', $this->mTitle, $reason, array() ); - - if ( $commit ) { - $dbw->commit(); - } - - return true; - } - - /** - * 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 $wgUser, then calls commitRollback() - * to do the dirty work - * - * @param $fromP String: Name of the user whose edits to rollback. - * @param $summary String: Custom summary. Set to default summary if empty. - * @param $token String: Rollback token. - * @param $bot Boolean: If true, mark all reverted edits as bot. - * - * @param $resultDetails Array: contains result-specific array of additional values - * 'alreadyrolled' : 'current' (rev) - * success : 'summary' (str), 'current' (rev), 'target' (rev) - * - * @return 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 ) { - global $wgUser; - - $resultDetails = null; - - # Check permissions - $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ); - $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ); - $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); - - if ( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) { - $errors[] = array( 'sessionfailure' ); - } - - if ( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) { - $errors[] = array( 'actionthrottledtext' ); - } - - # If there were errors, bail out now - if ( !empty( $errors ) ) { - return $errors; - } - - return $this->commitRollback( $fromP, $summary, $bot, $resultDetails ); - } - - /** - * 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. - */ - public function commitRollback( $fromP, $summary, $bot, &$resultDetails ) { - global $wgUseRCPatrol, $wgUser, $wgLang; - - $dbw = wfGetDB( DB_MASTER ); - - if ( wfReadOnly() ) { - return array( array( 'readonlytext' ) ); - } - - # Get the last editor - $current = Revision::newFromTitle( $this->mTitle ); - 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' ) ); - } else if ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) { - # Only admins can see this text - return array( array( 'notvisiblerev' ) ); - } - - $set = array(); - if ( $bot && $wgUser->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 > '{$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 = wfMsgForContent( 'revertpage-nouser' ); - } else { - $summary = wfMsgForContent( 'revertpage' ); - } - } - - # Allow the custom summary to use the same args as the default message - $args = array( - $target->getUserText(), $from, $s->rev_id, - $wgLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ), true ), - $current->getId(), $wgLang->timeanddate( $current->getTimestamp() ) - ); - $summary = wfMsgReplaceArgs( $summary, $args ); - - # Save - $flags = EDIT_UPDATE; - - if ( $wgUser->isAllowed( 'minoredit' ) ) { - $flags |= EDIT_MINOR; - } - - if ( $bot && ( $wgUser->isAllowed( 'markbotedits' ) || $wgUser->isAllowed( 'bot' ) ) ) { - $flags |= EDIT_FORCE_BOT; - } - - # Actually store the edit - $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId() ); - if ( !empty( $status->value['revision'] ) ) { - $revId = $status->value['revision']->getId(); - } else { - $revId = false; - } - - wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target, $current ) ); - - $resultDetails = array( - 'summary' => $summary, - 'current' => $current, - 'target' => $target, - 'newid' => $revId - ); - - return array(); - } - - /** - * User interface for rollback operations - */ - public function rollback() { - global $wgUser, $wgOut, $wgRequest; - - $details = null; - - $result = $this->doRollback( - $wgRequest->getVal( 'from' ), - $wgRequest->getText( 'summary' ), - $wgRequest->getVal( 'token' ), - $wgRequest->getBool( 'bot' ), - $details - ); - - if ( in_array( array( 'actionthrottledtext' ), $result ) ) { - $wgOut->rateLimited(); - return; - } - - if ( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ) { - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $errArray = $result[0]; - $errMsg = array_shift( $errArray ); - $wgOut->addWikiMsgArray( $errMsg, $errArray ); - - if ( isset( $details['current'] ) ) { - $current = $details['current']; - - if ( $current->getComment() != '' ) { - $wgOut->addWikiMsgArray( 'editcomment', array( - $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) ); - } - } - - return; - } - - # Display permissions errors before read-only message -- there's no - # point in misleading the user into thinking the inability to rollback - # is only temporary. - if ( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) { - # array_diff is completely broken for arrays of arrays, sigh. - # Remove any 'readonlytext' error manually. - $out = array(); - foreach ( $result as $error ) { - if ( $error != array( 'readonlytext' ) ) { - $out [] = $error; - } - } - $wgOut->showPermissionsErrorPage( $out ); - - return; - } - - if ( $result == array( array( 'readonlytext' ) ) ) { - $wgOut->readOnlyPage(); - - return; - } - - $current = $details['current']; - $target = $details['target']; - $newId = $details['newid']; - $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - - if ( $current->getUserText() === '' ) { - $old = wfMsg( 'rev-deleted-user' ); - } else { - $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() ) - . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() ); - } - - $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() ) - . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() ); - $wgOut->addHTML( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) ); - $wgOut->returnToMain( false, $this->mTitle ); - - if ( !$wgRequest->getBool( 'hidediff', false ) && !$wgUser->getBoolOption( 'norollbackdiff', false ) ) { - $de = new DifferenceEngine( $this->mTitle, $current->getId(), $newId, false, true ); - $de->showDiff( '', '' ); - } - } - - /** - * Do standard deferred updates after page view - */ - public function viewUpdates() { - global $wgDeferredUpdateList, $wgDisableCounters, $wgUser; - if ( wfReadOnly() ) { - return; - } - - # Don't update page view counters on views from bot users (bug 14044) - if ( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) && $this->getID() ) { - Article::incViewCount( $this->getID() ); - $u = new SiteStatsUpdate( 1, 0, 0 ); - array_push( $wgDeferredUpdateList, $u ); - } - - # Update newtalk / watchlist notification status - $wgUser->clearNotification( $this->mTitle ); - } - - /** - * Prepare text which is about to be saved. - * Returns a stdclass with source, pst and output members - */ - public function prepareTextForEdit( $text, $revid = null ) { - if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid ) { - // Already prepared - return $this->mPreparedEdit; - } - - global $wgParser; - - $edit = (object)array(); - $edit->revid = $revid; - $edit->newText = $text; - $edit->pst = $this->preSaveTransform( $text ); - $edit->popts = $this->getParserOptions(); - $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid ); - $edit->oldText = $this->getContent(); - - $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. - * - * @private - * @param $text String: New text of the article - * @param $summary String: Edit summary - * @param $minoredit Boolean: Minor edit - * @param $timestamp_of_pagechange Timestamp associated with the page change - * @param $newid Integer: rev_id value of the new revision - * @param $changed Boolean: Whether or not the content actually changed - */ - public function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) { - global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgEnableParserCache; - - wfProfileIn( __METHOD__ ); - - # Parse the text - # Be careful not to double-PST: $text is usually already PST-ed once - if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { - wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); - $editInfo = $this->prepareTextForEdit( $text, $newid ); - } 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 ); - } - - # Update the links tables - $u = new LinksUpdate( $this->mTitle, $editInfo->output ); - $u->doUpdate(); - - wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $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 - global $wgRCMaxAge; - - $dbw = wfGetDB( DB_MASTER ); - $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); - $recentchanges = $dbw->tableName( 'recentchanges' ); - $sql = "DELETE FROM $recentchanges WHERE rc_timestamp < '{$cutoff}'"; - - $dbw->query( $sql ); - } - } - - $id = $this->getID(); - $title = $this->mTitle->getPrefixedDBkey(); - $shortTitle = $this->mTitle->getDBkey(); - - if ( 0 == $id ) { - wfProfileOut( __METHOD__ ); - return; - } - - $u = new SiteStatsUpdate( 0, 1, $this->mGoodAdjustment, $this->mTotalAdjustment ); - array_push( $wgDeferredUpdateList, $u ); - $u = new SearchUpdate( $id, $title, $text ); - array_push( $wgDeferredUpdateList, $u ); - - # If this is another user's talk page, update newtalk - # Don't do this if $changed = false otherwise some idiot can null-edit a - # load of user talk pages and piss people off, nor if it's a minor edit - # by a properly-flagged bot. - if ( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed - && !( $minoredit && $wgUser->isAllowed( 'nominornewtalk' ) ) - ) { - if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) { - $other = User::newFromName( $shortTitle, false ); - if ( !$other ) { - wfDebug( __METHOD__ . ": invalid username\n" ); - } elseif ( User::isIP( $shortTitle ) ) { - // An anonymous user - $other->setNewtalk( true ); - } elseif ( $other->isLoggedIn() ) { - $other->setNewtalk( true ); - } else { - wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); - } - } - } - - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - $wgMessageCache->replace( $shortTitle, $text ); - } - - wfProfileOut( __METHOD__ ); - } - - /** - * Perform article updates on a special page creation. - * - * @param $rev Revision object - * - * @todo This is a shitty interface function. Kill it and replace the - * other shitty functions like editUpdates and such so it's not needed - * anymore. - */ - public function createUpdates( $rev ) { - $this->mGoodAdjustment = $this->isCountable( $rev->getText() ); - $this->mTotalAdjustment = 1; - $this->editUpdates( $rev->getText(), $rev->getComment(), - $rev->isMinor(), wfTimestamp(), $rev->getId(), true ); - } - - /** * Generate the navigation links when browsing through an article revisions * It shows the information as: * Revision as of \<date\>; view current revision @@ -3746,16 +1568,17 @@ class Article { } $revision = Revision::newFromId( $oldid ); + $timestamp = $revision->getTimestamp(); + + $current = ( $oldid == $this->mPage->getLatest() ); + $td = $wgLang->timeanddate( $timestamp, true ); + $tddate = $wgLang->date( $timestamp, true ); + $tdtime = $wgLang->time( $timestamp, true ); - $current = ( $oldid == $this->mLatest ); - $td = $wgLang->timeanddate( $this->mTimestamp, true ); - $tddate = $wgLang->date( $this->mTimestamp, true ); - $tdtime = $wgLang->time( $this->mTimestamp, true ); - $sk = $wgUser->getSkin(); $lnk = $current ? wfMsgHtml( 'currentrevisionlink' ) - : $sk->link( - $this->mTitle, + : Linker::link( + $this->getTitle(), wfMsgHtml( 'currentrevisionlink' ), array(), $extraParams, @@ -3763,8 +1586,8 @@ class Article { ); $curdiff = $current ? wfMsgHtml( 'diff' ) - : $sk->link( - $this->mTitle, + : Linker::link( + $this->getTitle(), wfMsgHtml( 'diff' ), array(), array( @@ -3773,10 +1596,10 @@ class Article { ) + $extraParams, array( 'known', 'noclasses' ) ); - $prev = $this->mTitle->getPreviousRevisionID( $oldid ) ; + $prev = $this->getTitle()->getPreviousRevisionID( $oldid ) ; $prevlink = $prev - ? $sk->link( - $this->mTitle, + ? Linker::link( + $this->getTitle(), wfMsgHtml( 'previousrevision' ), array(), array( @@ -3787,8 +1610,8 @@ class Article { ) : wfMsgHtml( 'previousrevision' ); $prevdiff = $prev - ? $sk->link( - $this->mTitle, + ? Linker::link( + $this->getTitle(), wfMsgHtml( 'diff' ), array(), array( @@ -3800,8 +1623,8 @@ class Article { : wfMsgHtml( 'diff' ); $nextlink = $current ? wfMsgHtml( 'nextrevision' ) - : $sk->link( - $this->mTitle, + : Linker::link( + $this->getTitle(), wfMsgHtml( 'nextrevision' ), array(), array( @@ -3812,8 +1635,8 @@ class Article { ); $nextdiff = $current ? wfMsgHtml( 'diff' ) - : $sk->link( - $this->mTitle, + : Linker::link( + $this->getTitle(), wfMsgHtml( 'diff' ), array(), array( @@ -3829,23 +1652,22 @@ class Article { $canHide = $wgUser->isAllowed( 'deleterevision' ); if ( $canHide || ( $revision->getVisibility() && $wgUser->isAllowed( 'deletedhistory' ) ) ) { if ( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) { - $cdel = $sk->revDeleteLinkDisabled( $canHide ); // rev was hidden from Sysops + $cdel = Linker::revDeleteLinkDisabled( $canHide ); // rev was hidden from Sysops } else { $query = array( 'type' => 'revision', - 'target' => $this->mTitle->getPrefixedDbkey(), + 'target' => $this->getTitle()->getPrefixedDbkey(), 'ids' => $oldid ); - $cdel = $sk->revDeleteLink( $query, $revision->isDeleted( File::DELETED_RESTRICTED ), $canHide ); + $cdel = Linker::revDeleteLink( $query, $revision->isDeleted( File::DELETED_RESTRICTED ), $canHide ); } $cdel .= ' '; } # Show user links if allowed to see them. If hidden, then show them only if requested... - $userlinks = $sk->revUserTools( $revision, !$unhide ); + $userlinks = Linker::revUserTools( $revision, !$unhide ); - $m = wfMsg( 'revision-info-current' ); - $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-' + $infomsg = $current && !wfMessage( 'revision-info-current' )->isDisabled() ? 'revision-info-current' : 'revision-info'; @@ -3867,20 +1689,6 @@ class Article { $wgOut->setSubtitle( $r ); } - /** - * This function is called right before saving the wikitext, - * so we can do things like signatures and links-in-context. - * - * @param $text String article contents - * @return string article contents with altered wikitext markup (signatures - * converted, {{subst:}}, templates, etc.) - */ - public function preSaveTransform( $text ) { - global $wgParser, $wgUser; - - return $wgParser->preSaveTransform( $text, $this->mTitle, $wgUser, ParserOptions::newFromUser( $wgUser ) ); - } - /* Caching functions */ /** @@ -3900,8 +1708,8 @@ class Article { $called = true; if ( $this->isFileCacheable() ) { - $cache = new HTMLFileCache( $this->mTitle ); - if ( $cache->isFileCacheGood( $this->mTouched ) ) { + $cache = new HTMLFileCache( $this->getTitle() ); + if ( $cache->isFileCacheGood( $this->mPage->getTouched() ) ) { wfDebug( "Article::tryFileCache(): about to load file\n" ); $cache->loadFromFileCache(); return true; @@ -3924,7 +1732,7 @@ class Article { $cacheable = false; if ( HTMLFileCache::useFileCache() ) { - $cacheable = $this->getID() && !$this->mRedirectedFrom && !$this->mTitle->isRedirect(); + $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 ) ); @@ -3934,462 +1742,77 @@ class Article { return $cacheable; } - /** - * Loads page_touched and returns a value indicating if it should be used - * @return boolean 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_latest field - * @return integer rev_id of current revision - */ - public function getLatest() { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } - - return (int)$this->mLatest; - } - - /** - * 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 $text String: text submitted - * @param $comment String: comment submitted - * @param $minor Boolean: whereas it's a minor modification - */ - public function quickEdit( $text, $comment = '', $minor = 0 ) { - wfProfileIn( __METHOD__ ); - - $dbw = wfGetDB( DB_MASTER ); - $revision = new Revision( array( - 'page' => $this->getId(), - 'text' => $text, - 'comment' => $comment, - 'minor_edit' => $minor ? 1 : 0, - ) ); - $revision->insertOn( $dbw ); - $this->updateRevisionOn( $dbw, $revision ); - - global $wgUser; - wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $wgUser ) ); - - wfProfileOut( __METHOD__ ); - } - - /** - * Used to increment the view counter - * - * @param $id Integer: article id - */ - public static function incViewCount( $id ) { - $id = intval( $id ); - - global $wgHitcounterUpdateFreq; - - $dbw = wfGetDB( DB_MASTER ); - $pageTable = $dbw->tableName( 'page' ); - $hitcounterTable = $dbw->tableName( 'hitcounter' ); - $acchitsTable = $dbw->tableName( 'acchits' ); - $dbType = $dbw->getType(); - - if ( $wgHitcounterUpdateFreq <= 1 || $dbType == 'sqlite' ) { - $dbw->query( "UPDATE $pageTable SET page_counter = page_counter + 1 WHERE page_id = $id" ); - - return; - } - - # Not important enough to warrant an error page in case of failure - $oldignore = $dbw->ignoreErrors( true ); - - $dbw->query( "INSERT INTO $hitcounterTable (hc_id) VALUES ({$id})" ); - - $checkfreq = intval( $wgHitcounterUpdateFreq / 25 + 1 ); - if ( ( rand() % $checkfreq != 0 ) or ( $dbw->lastErrno() != 0 ) ) { - # Most of the time (or on SQL errors), skip row count check - $dbw->ignoreErrors( $oldignore ); - - return; - } - - $res = $dbw->query( "SELECT COUNT(*) as n FROM $hitcounterTable" ); - $row = $dbw->fetchObject( $res ); - $rown = intval( $row->n ); - - if ( $rown >= $wgHitcounterUpdateFreq ) { - wfProfileIn( 'Article::incViewCount-collect' ); - $old_user_abort = ignore_user_abort( true ); - - $dbw->lockTables( array(), array( 'hitcounter' ), __METHOD__, false ); - $tabletype = $dbType == 'mysql' ? "ENGINE=HEAP " : ''; - $dbw->query( "CREATE TEMPORARY TABLE $acchitsTable $tabletype AS " . - "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable " . - 'GROUP BY hc_id', __METHOD__ ); - $dbw->delete( 'hitcounter', '*', __METHOD__ ); - $dbw->unlockTables( __METHOD__ ); - - if ( $dbType == 'mysql' ) { - $dbw->query( "UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n " . - 'WHERE page_id = hc_id', __METHOD__ ); - } else { - $dbw->query( "UPDATE $pageTable SET page_counter=page_counter + hc_n " . - "FROM $acchitsTable WHERE page_id = hc_id", __METHOD__ ); - } - $dbw->query( "DROP TABLE $acchitsTable", __METHOD__ ); - - ignore_user_abort( $old_user_abort ); - wfProfileOut( 'Article::incViewCount-collect' ); - } - - $dbw->ignoreErrors( $oldignore ); - } - - /**#@+ - * 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 a title object - */ - 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 - */ - public static function onArticleDelete( $title ) { - global $wgMessageCache; - - # 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 ); - - # Messages - if ( $title->getNamespace() == NS_MEDIAWIKI ) { - $wgMessageCache->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 ); - $user->setNewtalk( false ); - } - - # Image redirects - RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); - } - - /** - * Purge caches on page update etc - * - * @param $title Title object - * @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 ) { - global $wgDeferredUpdateList; - - // Invalidate caches of articles which include this page - $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' ); - - // Invalidate the caches of all pages which redirect here - $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' ); - - # Purge squid for this page only - $title->purgeSquid(); - - # Clear file cache for this page only - HTMLFileCache::clearFileCache( $title ); - } - /**#@-*/ /** - * Overriden by ImagePage class, only present here to avoid a fatal error - * Called for ?action=revert + * Add the primary page-view wikitext to the output buffer + * Saves the text into the parser cache if possible. + * Updates templatelinks if it is out of date. + * + * @param $text String + * @param $cache Boolean + * @param $parserOptions mixed ParserOptions object, or boolean false */ - public function revert() { + public function outputWikiText( $text, $cache = true, $parserOptions = false ) { global $wgOut; - $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); - } - - /** - * Info about this page - * Called for ?action=info when $wgAllowPageInfo is on. - */ - public function info() { - global $wgLang, $wgOut, $wgAllowPageInfo, $wgUser; - - if ( !$wgAllowPageInfo ) { - $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); - return; - } - - $page = $this->mTitle->getSubjectPage(); - - $wgOut->setPagetitle( $page->getPrefixedText() ); - $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) ); - $wgOut->setSubtitle( wfMsgHtml( 'infosubtitle' ) ); - - if ( !$this->mTitle->exists() ) { - $wgOut->addHTML( '<div class="noarticletext">' ); - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - // This doesn't quite make sense; the user is asking for - // information about the _page_, not the message... -- RC - $wgOut->addHTML( htmlspecialchars( wfMsgWeirdKey( $this->mTitle->getText() ) ) ); - } else { - $msg = $wgUser->isLoggedIn() - ? 'noarticletext' - : 'noarticletextanon'; - $wgOut->addHTML( wfMsgExt( $msg, 'parse' ) ); - } - $wgOut->addHTML( '</div>' ); - } else { - $dbr = wfGetDB( DB_SLAVE ); - $wl_clause = array( - 'wl_title' => $page->getDBkey(), - 'wl_namespace' => $page->getNamespace() ); - $numwatchers = $dbr->selectField( - 'watchlist', - 'COUNT(*)', - $wl_clause, - __METHOD__, - $this->getSelectOptions() ); - - $pageInfo = $this->pageCountInfo( $page ); - $talkInfo = $this->pageCountInfo( $page->getTalkPage() ); - - - //FIXME: unescaped messages - $wgOut->addHTML( "<ul><li>" . wfMsg( "numwatchers", $wgLang->formatNum( $numwatchers ) ) . '</li>' ); - $wgOut->addHTML( "<li>" . wfMsg( 'numedits', $wgLang->formatNum( $pageInfo['edits'] ) ) . '</li>' ); - - if ( $talkInfo ) { - $wgOut->addHTML( '<li>' . wfMsg( "numtalkedits", $wgLang->formatNum( $talkInfo['edits'] ) ) . '</li>' ); - } - - $wgOut->addHTML( '<li>' . wfMsg( "numauthors", $wgLang->formatNum( $pageInfo['authors'] ) ) . '</li>' ); + $this->mParserOutput = $this->getOutputFromWikitext( $text, $cache, $parserOptions ); - if ( $talkInfo ) { - $wgOut->addHTML( '<li>' . wfMsg( 'numtalkauthors', $wgLang->formatNum( $talkInfo['authors'] ) ) . '</li>' ); - } + $this->doCascadeProtectionUpdates( $this->mParserOutput ); - $wgOut->addHTML( '</ul>' ); - } + $wgOut->addParserOutput( $this->mParserOutput ); } /** - * Return the total number of edits and number of unique editors - * on a given page. If page does not exist, returns false. + * 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. * - * @param $title Title object - * @return mixed array or boolean false - */ - public function pageCountInfo( $title ) { - $id = $title->getArticleId(); - - if ( $id == 0 ) { - return false; - } - - $dbr = wfGetDB( DB_SLAVE ); - $rev_clause = array( 'rev_page' => $id ); - $edits = $dbr->selectField( - 'revision', - 'COUNT(rev_page)', - $rev_clause, - __METHOD__, - $this->getSelectOptions() - ); - $authors = $dbr->selectField( - 'revision', - 'COUNT(DISTINCT rev_user_text)', - $rev_clause, - __METHOD__, - $this->getSelectOptions() - ); - - return array( 'edits' => $edits, 'authors' => $authors ); - } - - /** - * Return a list of templates used by this article. - * Uses the templatelinks table + * @since 1.16 (r52326) for LiquidThreads * - * @return Array of Title objects + * @param $oldid mixed integer Revision ID or null + * @param $user User The relevant user + * @return ParserOutput or false if the given revsion ID is not found */ - public function getUsedTemplates() { - $result = array(); - $id = $this->mTitle->getArticleID(); - - if ( $id == 0 ) { - return array(); - } - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'templatelinks' ), - array( 'tl_namespace', 'tl_title' ), - array( 'tl_from' => $id ), - __METHOD__ ); - - if ( $res !== false ) { - foreach ( $res as $row ) { - $result[] = Title::makeTitle( $row->tl_namespace, $row->tl_title ); - } - } + public function getParserOutput( $oldid = null, User $user = null ) { + global $wgEnableParserCache, $wgUser; + $user = is_null( $user ) ? $wgUser : $user; - return $result; - } + wfProfileIn( __METHOD__ ); + // Should the parser cache be used? + $useParserCache = $wgEnableParserCache && + $user->getStubThreshold() == 0 && + $this->mPage->exists() && + $oldid === null; - /** - * Returns a list of hidden categories this page is a member of. - * Uses the page_props and categorylinks tables. - * - * @return Array of Title objects - */ - public function getHiddenCategories() { - $result = array(); - $id = $this->mTitle->getArticleID(); + wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); - if ( $id == 0 ) { - return array(); + if ( $user->getStubThreshold() ) { + wfIncrStats( 'pcache_miss_stub' ); } - $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 ); + if ( $useParserCache ) { + $parserOutput = ParserCache::singleton()->get( $this, $this->mPage->getParserOptions() ); + if ( $parserOutput !== false ) { + wfProfileOut( __METHOD__ ); + return $parserOutput; } } - return $result; - } - - /** - * Return an applicable autosummary if one exists for the given edit. - * @param $oldtext String: the previous text of the page. - * @param $newtext String: The submitted text of the page. - * @param $flags Bitmask: a bitmask of flags submitted for the edit. - * @return string An appropriate autosummary, or an empty string. - */ - public static function getAutosummary( $oldtext, $newtext, $flags ) { - global $wgContLang; - - # Decide what kind of autosummary is needed. - - # Redirect autosummaries - $ot = Title::newFromRedirect( $oldtext ); - $rt = Title::newFromRedirect( $newtext ); - - if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { - return wfMsgForContent( 'autoredircomment', $rt->getFullText() ); - } - - # New page autosummaries - if ( $flags & EDIT_NEW && strlen( $newtext ) ) { - # If they're making a new article, give its text, truncated, in the summary. - - $truncatedtext = $wgContLang->truncate( - str_replace( "\n", ' ', $newtext ), - max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) ); - - return wfMsgForContent( 'autosumm-new', $truncatedtext ); - } - - # Blanking autosummaries - if ( $oldtext != '' && $newtext == '' ) { - return wfMsgForContent( 'autosumm-blank' ); - } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) { - # Removing more than 90% of the article - - $truncatedtext = $wgContLang->truncate( - $newtext, - max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) ); - - return wfMsgForContent( 'autosumm-replace', $truncatedtext ); + // Cache miss; parse and output it. + if ( $oldid === null ) { + $text = $this->mPage->getRawText(); + } else { + $rev = Revision::newFromTitle( $this->getTitle(), $oldid ); + if ( $rev === null ) { + wfProfileOut( __METHOD__ ); + return false; + } + $text = $rev->getText(); } - # If we reach this point, there's no applicable autosummary for our case, so our - # autosummary is empty. - return ''; - } - - /** - * Add the primary page-view wikitext to the output buffer - * Saves the text into the parser cache if possible. - * Updates templatelinks if it is out of date. - * - * @param $text String - * @param $cache Boolean - * @param $parserOptions mixed ParserOptions object, or boolean false - */ - public function outputWikiText( $text, $cache = true, $parserOptions = false ) { - global $wgOut; - - $this->mParserOutput = $this->getOutputFromWikitext( $text, $cache, $parserOptions ); - $wgOut->addParserOutput( $this->mParserOutput ); + wfProfileOut( __METHOD__ ); + return $this->getOutputFromWikitext( $text, $useParserCache ); } /** @@ -4400,24 +1823,24 @@ class Article { * @param $text string * @param $cache boolean * @param $parserOptions parsing options, defaults to false - * @return string containing parsed output + * @return ParserOutput */ public function getOutputFromWikitext( $text, $cache = true, $parserOptions = false ) { global $wgParser, $wgEnableParserCache, $wgUseFileCache; if ( !$parserOptions ) { - $parserOptions = $this->getParserOptions(); + $parserOptions = $this->mPage->getParserOptions(); } $time = - wfTime(); - $this->mParserOutput = $wgParser->parse( $text, $this->mTitle, + $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions, true, true, $this->getRevIdFetched() ); $time += wfTime(); # Timing hack if ( $time > 3 ) { wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, - $this->mTitle->getPrefixedDBkey() ) ); + $this->getTitle()->getPrefixedDBkey() ) ); } if ( $wgEnableParserCache && $cache && $this->mParserOutput->isCacheable() ) { @@ -4432,213 +1855,156 @@ class Article { $wgUseFileCache = false; } - $this->doCascadeProtectionUpdates( $this->mParserOutput ); + if ( $this->isCurrent() ) { + $this->mPage->doCascadeProtectionUpdates( $this->mParserOutput ); + } return $this->mParserOutput; } /** - * Get parser options suitable for rendering the primary article wikitext - * @return mixed ParserOptions object or boolean false + * Sets the context this Article is executed in + * + * @param $context IContextSource + * @since 1.18 */ - public function getParserOptions() { - global $wgUser; - - if ( !$this->mParserOptions ) { - $this->mParserOptions = new ParserOptions( $wgUser ); - $this->mParserOptions->setTidy( true ); - $this->mParserOptions->enableLimitReport(); - } - - // Clone to allow modifications of the return value without affecting - // the cache - return clone $this->mParserOptions; + public function setContext( $context ) { + $this->mContext = $context; } /** - * Updates cascading protections + * Gets the context this Article is executed in * - * @param $parserOutput mixed ParserOptions object, or boolean false - **/ - protected function doCascadeProtectionUpdates( $parserOutput ) { - if ( !$this->isCurrent() || wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { - return; - } - - // templatelinks table 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 - $id = $this->mTitle->getArticleID(); - - $tlTemplates = array(); - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'templatelinks' ), - array( 'tl_namespace', 'tl_title' ), - array( 'tl_from' => $id ), - __METHOD__ - ); - - foreach ( $res as $row ) { - $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; - } - - # Get templates from parser output. - $poTemplates = array(); - foreach ( $parserOutput->getTemplates() as $ns => $templates ) { - foreach ( $templates as $dbk => $id ) { - $poTemplates["$ns:$dbk"] = true; - } - } - - # Get the diff - $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); - - if ( count( $templates_diff ) > 0 ) { - # Whee, link updates time. - $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); - $u->doUpdate(); + * @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(); } } /** - * Update all the appropriate counts in the category table, given that - * we've added the categories $added and deleted the categories $deleted. + * Use PHP's magic __get handler to handle accessing of + * raw WikiPage fields for backwards compatibility. * - * @param $added array The names of categories that were added - * @param $deleted array The names of categories that were deleted + * @param $fname String Field name */ - public function updateCategoryCounts( $added, $deleted ) { - $ns = $this->mTitle->getNamespace(); - $dbw = wfGetDB( DB_MASTER ); - - # First make sure the rows exist. If one of the "deleted" ones didn't - # exist, we might legitimately not create it, but it's simpler to just - # create it and then give it a negative value, since the value is bogus - # anyway. - # - # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. - $insertCats = array_merge( $added, $deleted ); - if ( !$insertCats ) { - # Okay, nothing to do - return; - } - - $insertRows = array(); - - foreach ( $insertCats as $cat ) { - $insertRows[] = array( - 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ), - 'cat_title' => $cat - ); - } - $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' ); - - $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 ( $added ) { - $dbw->update( - 'category', - $addFields, - array( 'cat_title' => $added ), - __METHOD__ - ); + 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 ); + } - if ( $deleted ) { - $dbw->update( - 'category', - $removeFields, - array( 'cat_title' => $deleted ), - __METHOD__ - ); + /** + * Use PHP's magic __set handler to handle setting of + * raw WikiPage fields for backwards compatibility. + * + * @param $fname String Field name + * @param $fvalue mixed New value + * @param $args Array Arguments to the method + */ + 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 __get(): ' . $fname, E_USER_NOTICE ); } } /** - * 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 Article::view is forced to - * consider, so it's not appropriate to use there. + * Use PHP's magic __call handler to transform instance calls to + * WikiPage functions for backwards compatibility. * - * @since 1.16 (r52326) for LiquidThreads - * - * @param $oldid mixed integer Revision ID or null + * @param $fname String Name of called method + * @param $args Array Arguments to the method */ - public function getParserOutput( $oldid = null ) { - global $wgEnableParserCache, $wgUser; + 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 ); + } - // Should the parser cache be used? - $useParserCache = $wgEnableParserCache && - $wgUser->getStubThreshold() == 0 && - $this->exists() && - $oldid === null; + // ****** B/C functions to work-around PHP silliness with __call and references ****** // + public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) { + return $this->mPage->updateRestrictions( $limit, $reason, $cascade, $expiry ); + } - wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); + public function doDeleteArticle( $reason, $suppress = false, $id = 0, $commit = true, &$error = '' ) { + return $this->mPage->doDeleteArticle( $reason, $suppress, $id, $commit, $error ); + } - if ( $wgUser->getStubThreshold() ) { - wfIncrStats( 'pcache_miss_stub' ); - } + public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user = null ) { + global $wgUser; + $user = is_null( $user ) ? $wgUser : $user; + return $this->mPage->doRollback( $fromP, $summary, $token, $bot, $resultDetails, $user ); + } - $parserOutput = false; - if ( $useParserCache ) { - $parserOutput = ParserCache::singleton()->get( $this, $this->getParserOptions() ); - } + public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser = null ) { + global $wgUser; + $guser = is_null( $guser ) ? $wgUser : $guser; + return $this->mPage->commitRollback( $fromP, $summary, $bot, $resultDetails, $guser ); + } - if ( $parserOutput === false ) { - // Cache miss; parse and output it. - $rev = Revision::newFromTitle( $this->getTitle(), $oldid ); + public function generateReason( &$hasHistory ) { + return $this->mPage->getAutoDeleteReason( $hasHistory ); + } - return $this->getOutputFromWikitext( $rev->getText(), $useParserCache ); - } else { - return $parserOutput; - } + // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** // + public static function selectFields() { + return WikiPage::selectFields(); } - // Deprecated methods - /** - * Get the database which should be used for reads - * - * @return Database - * @deprecated - just call wfGetDB( DB_MASTER ) instead - */ - function getDB() { - wfDeprecated( __METHOD__ ); - return wfGetDB( DB_MASTER ); + public static function onArticleCreate( $title ) { + return WikiPage::onArticleCreate( $title ); + } + + public static function onArticleDelete( $title ) { + return WikiPage::onArticleDelete( $title ); + } + + public static function onArticleEdit( $title ) { + return WikiPage::onArticleEdit( $title ); } + public static function getAutosummary( $oldtext, $newtext, $flags ) { + return WikiPage::getAutosummary( $oldtext, $newtext, $flags ); + } + // ****** } class PoolWorkArticleView extends PoolCounterWork { + + /** + * @var Article + */ private $mArticle; - + function __construct( $article, $key, $useParserCache, $parserOptions ) { parent::__construct( 'ArticleView', $key ); $this->mArticle = $article; $this->cacheable = $useParserCache; $this->parserOptions = $parserOptions; } - + function doWork() { return $this->mArticle->doViewParse(); } - + function getCachedWork() { global $wgOut; - + $parserCache = ParserCache::singleton(); $this->mArticle->mParserOutput = $parserCache->get( $this->mArticle, $this->parserOptions ); @@ -4652,21 +2018,24 @@ class PoolWorkArticleView extends PoolCounterWork { } return false; } - + function fallback() { return $this->mArticle->tryDirtyCache(); } - + + /** + * @param $status Status + */ function error( $status ) { global $wgOut; $wgOut->clearHTML(); // for release() errors $wgOut->enableClientCache( false ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); - + $errortext = $status->getWikiText( false, 'view-pool-error' ); $wgOut->addWikiText( '<div class="errorbox">' . $errortext . '</div>' ); - + return false; } } diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index 7dc99259..eebb52d6 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -131,6 +131,8 @@ class AuthPlugin { * and use the same keys. 'Realname' 'Emailaddress' and 'Nickname' * all reference this. * + * @param $prop string + * * @return Boolean */ public function allowPropChange( $prop = '' ) { @@ -254,10 +256,21 @@ class AuthPlugin { * Get an instance of a User object * * @param $user User + * + * @return AuthPluginUser */ public function getUserInstance( User &$user ) { return new AuthPluginUser( $user ); } + + /** + * Get a list of domains (in HTMLForm options format) used. + * + * @return array + */ + public function domainList() { + return array(); + } } class AuthPluginUser { diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 347ed694..134e53ea 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -14,20 +14,18 @@ global $wgAutoloadLocalClasses; $wgAutoloadLocalClasses = array( # Includes + 'Action' => 'includes/Action.php', 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', 'AjaxResponse' => 'includes/AjaxResponse.php', 'AlphabeticPager' => 'includes/Pager.php', - 'APCBagOStuff' => 'includes/BagOStuff.php', 'Article' => 'includes/Article.php', 'AtomFeed' => 'includes/Feed.php', 'AuthPlugin' => 'includes/AuthPlugin.php', 'AuthPluginUser' => 'includes/AuthPlugin.php', 'Autopromote' => 'includes/Autopromote.php', 'BacklinkCache' => 'includes/BacklinkCache.php', - 'BagOStuff' => 'includes/BagOStuff.php', + 'BaseTemplate' => 'includes/SkinTemplate.php', 'Block' => 'includes/Block.php', - 'CacheDependency' => 'includes/CacheDependency.php', - 'CacheTime' => 'includes/parser/ParserOutput.php', 'Category' => 'includes/Category.php', 'Categoryfinder' => 'includes/Categoryfinder.php', 'CategoryPage' => 'includes/CategoryPage.php', @@ -39,28 +37,22 @@ $wgAutoloadLocalClasses = array( 'CdbWriter' => 'includes/Cdb.php', 'CdbWriter_DBA' => 'includes/Cdb.php', 'CdbWriter_PHP' => 'includes/Cdb_PHP.php', - 'ChangesList' => 'includes/ChangesList.php', 'ChangesFeed' => 'includes/ChangesFeed.php', + 'ChangesList' => 'includes/ChangesList.php', 'ChangeTags' => 'includes/ChangeTags.php', 'ChannelFeed' => 'includes/Feed.php', 'Collation' => 'includes/Collation.php', - 'Cookie' => 'includes/HttpFunctions.php', - 'CookieJar' => 'includes/HttpFunctions.php', 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', 'ConfEditor' => 'includes/ConfEditor.php', 'ConfEditorParseError' => 'includes/ConfEditor.php', 'ConfEditorToken' => 'includes/ConfEditor.php', - 'ConstantDependency' => 'includes/CacheDependency.php', - 'CreativeCommonsRdf' => 'includes/Metadata.php', - 'Credits' => 'includes/Credits.php', - 'CSSJanus' => 'includes/libs/CSSJanus.php', - 'CSSMin' => 'includes/libs/CSSMin.php', - 'DBABagOStuff' => 'includes/BagOStuff.php', - 'DependencyWrapper' => 'includes/CacheDependency.php', + 'ContextSource' => 'includes/RequestContext.php', + 'Cookie' => 'includes/Cookie.php', + 'CookieJar' => 'includes/Cookie.php', 'DiffHistoryBlob' => 'includes/HistoryBlob.php', 'DjVuImage' => 'includes/DjVuImage.php', 'DoubleReplacer' => 'includes/StringUtils.php', - 'DublinCoreRdf' => 'includes/Metadata.php', + 'DummyLinker' => 'includes/Linker.php', 'Dump7ZipOutput' => 'includes/Export.php', 'DumpBZip2Output' => 'includes/Export.php', 'DumpFileOutput' => 'includes/Export.php', @@ -72,145 +64,133 @@ $wgAutoloadLocalClasses = array( 'DumpNotalkFilter' => 'includes/Export.php', 'DumpOutput' => 'includes/Export.php', 'DumpPipeOutput' => 'includes/Export.php', - 'eAccelBagOStuff' => 'includes/BagOStuff.php', 'EditPage' => 'includes/EditPage.php', 'EmailNotification' => 'includes/UserMailer.php', 'EnhancedChangesList' => 'includes/ChangesList.php', 'ErrorPageError' => 'includes/Exception.php', - 'Exif' => 'includes/Exif.php', 'ExplodeIterator' => 'includes/StringUtils.php', 'ExternalEdit' => 'includes/ExternalEdit.php', + 'ExternalStore' => 'includes/ExternalStore.php', 'ExternalStoreDB' => 'includes/ExternalStoreDB.php', 'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php', - 'ExternalStore' => 'includes/ExternalStore.php', 'ExternalUser' => 'includes/ExternalUser.php', - 'FatalError' => 'includes/Exception.php', 'FakeTitle' => 'includes/FakeTitle.php', - 'FakeMemCachedClient' => 'includes/ObjectCache.php', + 'Fallback' => 'includes/Fallback.php', + 'FatalError' => 'includes/Exception.php', 'FauxRequest' => 'includes/WebRequest.php', 'FauxResponse' => 'includes/WebResponse.php', 'FeedItem' => 'includes/Feed.php', 'FeedUtils' => 'includes/FeedUtils.php', 'FileDeleteForm' => 'includes/FileDeleteForm.php', - 'FileDependency' => 'includes/CacheDependency.php', - 'FileRevertForm' => 'includes/FileRevertForm.php', 'ForkController' => 'includes/ForkController.php', - 'FormatExif' => 'includes/Exif.php', + 'FormlessAction' => 'includes/Action.php', + 'FormAction' => 'includes/Action.php', 'FormOptions' => 'includes/FormOptions.php', - 'GlobalDependency' => 'includes/CacheDependency.php', - 'HashBagOStuff' => 'includes/BagOStuff.php', + 'FormSpecialPage' => 'includes/SpecialPage.php', + 'GenderCache' => 'includes/GenderCache.php', 'HashtableReplacer' => 'includes/StringUtils.php', - 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', 'HistoryBlob' => 'includes/HistoryBlob.php', + 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', 'HistoryBlobStub' => 'includes/HistoryBlob.php', 'HistoryPage' => 'includes/HistoryPage.php', 'HistoryPager' => 'includes/HistoryPage.php', + 'Hooks' => 'includes/Hooks.php', 'Html' => 'includes/Html.php', - 'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php', - 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', - 'HTMLFileCache' => 'includes/HTMLFileCache.php', + 'HTMLCheckField' => 'includes/HTMLForm.php', + 'HTMLEditTools' => 'includes/HTMLForm.php', + 'HTMLFloatField' => 'includes/HTMLForm.php', 'HTMLForm' => 'includes/HTMLForm.php', 'HTMLFormField' => 'includes/HTMLForm.php', - 'HTMLTextField' => 'includes/HTMLForm.php', - 'HTMLIntField' => 'includes/HTMLForm.php', - 'HTMLTextAreaField' => 'includes/HTMLForm.php', - 'HTMLFloatField' => 'includes/HTMLForm.php', 'HTMLHiddenField' => 'includes/HTMLForm.php', - 'HTMLSubmitField' => 'includes/HTMLForm.php', - 'HTMLEditTools' => 'includes/HTMLForm.php', - 'HTMLCheckField' => 'includes/HTMLForm.php', - 'HTMLSelectField' => 'includes/HTMLForm.php', - 'HTMLSelectOrOtherField' => 'includes/HTMLForm.php', + 'HTMLInfoField' => 'includes/HTMLForm.php', + 'HTMLIntField' => 'includes/HTMLForm.php', 'HTMLMultiSelectField' => 'includes/HTMLForm.php', 'HTMLRadioField' => 'includes/HTMLForm.php', - 'HTMLInfoField' => 'includes/HTMLForm.php', + 'HTMLSelectAndOtherField' => 'includes/HTMLForm.php', + 'HTMLSelectField' => 'includes/HTMLForm.php', + 'HTMLSelectOrOtherField' => 'includes/HTMLForm.php', + 'HTMLSubmitField' => 'includes/HTMLForm.php', + 'HTMLTextAreaField' => 'includes/HTMLForm.php', + 'HTMLTextField' => 'includes/HTMLForm.php', 'Http' => 'includes/HttpFunctions.php', 'HttpRequest' => 'includes/HttpFunctions.old.php', + 'IContextSource' => 'includes/RequestContext.php', 'IcuCollation' => 'includes/Collation.php', + 'IdentityCollation' => 'includes/Collation.php', 'ImageGallery' => 'includes/ImageGallery.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', 'IncludableSpecialPage' => 'includes/SpecialPage.php', 'IndexPager' => 'includes/Pager.php', - 'Interwiki' => 'includes/Interwiki.php', + 'Interwiki' => 'includes/interwiki/Interwiki.php', 'IP' => 'includes/IP.php', - 'JavaScriptMinifier' => 'includes/libs/JavaScriptMinifier.php', - 'LCStore_DB' => 'includes/LocalisationCache.php', 'LCStore_CDB' => 'includes/LocalisationCache.php', + 'LCStore_DB' => 'includes/LocalisationCache.php', 'LCStore_Null' => 'includes/LocalisationCache.php', + 'LegacyTemplate' => 'includes/SkinLegacy.php', 'License' => 'includes/Licenses.php', 'Licenses' => 'includes/Licenses.php', - 'LinkBatch' => 'includes/LinkBatch.php', - 'LinkCache' => 'includes/LinkCache.php', 'Linker' => 'includes/Linker.php', 'LinkFilter' => 'includes/LinkFilter.php', 'LinksUpdate' => 'includes/LinksUpdate.php', 'LocalisationCache' => 'includes/LocalisationCache.php', 'LocalisationCache_BulkLoad' => 'includes/LocalisationCache.php', + 'LogEventsList' => 'includes/LogEventsList.php', 'LogPage' => 'includes/LogPage.php', 'LogPager' => 'includes/LogEventsList.php', - 'LogEventsList' => 'includes/LogEventsList.php', - 'LogReader' => 'includes/LogEventsList.php', - 'LogViewer' => 'includes/LogEventsList.php', - 'MacBinary' => 'includes/MacBinary.php', - 'MagicWordArray' => 'includes/MagicWord.php', 'MagicWord' => 'includes/MagicWord.php', + 'MagicWordArray' => 'includes/MagicWord.php', 'MailAddress' => 'includes/UserMailer.php', - 'MathRenderer' => 'includes/Math.php', - 'MediaWikiBagOStuff' => 'includes/BagOStuff.php', - 'MediaWiki_I18N' => 'includes/SkinTemplate.php', 'MediaWiki' => 'includes/Wiki.php', - 'MemCachedClientforWiki' => 'includes/memcached-client.php', + 'MediaWiki_I18N' => 'includes/SkinTemplate.php', 'Message' => 'includes/Message.php', 'MessageBlobStore' => 'includes/MessageBlobStore.php', - 'MessageCache' => 'includes/MessageCache.php', 'MimeMagic' => 'includes/MimeMagic.php', 'MWException' => 'includes/Exception.php', + 'MWExceptionHandler' => 'includes/Exception.php', + 'MWFunction' => 'includes/MWFunction.php', 'MWHttpRequest' => 'includes/HttpFunctions.php', - 'MWMemcached' => 'includes/memcached-client.php', + 'MWInit' => 'includes/Init.php', 'MWNamespace' => 'includes/Namespace.php', 'OldChangesList' => 'includes/ChangesList.php', 'OutputPage' => 'includes/OutputPage.php', - 'PageQueryPage' => 'includes/PageQueryPage.php', + 'Page' => 'includes/WikiPage.php', 'PageHistory' => 'includes/HistoryPage.php', 'PageHistoryPager' => 'includes/HistoryPage.php', + 'PageQueryPage' => 'includes/PageQueryPage.php', 'Pager' => 'includes/Pager.php', 'PasswordError' => 'includes/User.php', 'PatrolLog' => 'includes/PatrolLog.php', + 'PermissionsError' => 'includes/Exception.php', 'PhpHttpRequest' => 'includes/HttpFunctions.php', 'PoolCounter' => 'includes/PoolCounter.php', 'PoolCounter_Stub' => 'includes/PoolCounter.php', 'PoolCounterWork' => 'includes/PoolCounter.php', 'Preferences' => 'includes/Preferences.php', + 'PreferencesForm' => 'includes/Preferences.php', 'PrefixSearch' => 'includes/PrefixSearch.php', - 'Profiler' => 'includes/Profiler.php', - 'ProfilerSimple' => 'includes/ProfilerSimple.php', - 'ProfilerSimpleText' => 'includes/ProfilerSimpleText.php', - 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php', 'ProtectionForm' => 'includes/ProtectionForm.php', 'QueryPage' => 'includes/QueryPage.php', 'QuickTemplate' => 'includes/SkinTemplate.php', 'RawPage' => 'includes/RawPage.php', 'RCCacheEntry' => 'includes/ChangesList.php', 'RdfMetaData' => 'includes/Metadata.php', + 'ReadOnlyError' => 'includes/Exception.php', 'RecentChange' => 'includes/RecentChange.php', + 'RedirectSpecialPage' => 'includes/SpecialPage.php', 'RegexlikeReplacer' => 'includes/StringUtils.php', 'ReplacementArray' => 'includes/StringUtils.php', 'Replacer' => 'includes/StringUtils.php', - 'ResourceLoader' => 'includes/resourceloader/ResourceLoader.php', - 'ResourceLoaderContext' => 'includes/resourceloader/ResourceLoaderContext.php', - 'ResourceLoaderModule' => 'includes/resourceloader/ResourceLoaderModule.php', - 'ResourceLoaderWikiModule' => 'includes/resourceloader/ResourceLoaderWikiModule.php', - 'ResourceLoaderFileModule' => 'includes/resourceloader/ResourceLoaderFileModule.php', - 'ResourceLoaderSiteModule' => 'includes/resourceloader/ResourceLoaderSiteModule.php', - 'ResourceLoaderUserModule' => 'includes/resourceloader/ResourceLoaderUserModule.php', - 'ResourceLoaderUserOptionsModule' => 'includes/resourceloader/ResourceLoaderUserOptionsModule.php', - 'ResourceLoaderStartUpModule' => 'includes/resourceloader/ResourceLoaderStartUpModule.php', + 'RequestContext' => 'includes/RequestContext.php', 'ReverseChronologicalPager' => 'includes/Pager.php', + 'Rev_Item' => 'includes/RevisionList.php', + 'Rev_List' => 'includes/RevisionList.php', 'Revision' => 'includes/Revision.php', - 'RevisionDelete' => 'includes/revisiondelete/RevisionDelete.php', + 'RevisionList' => 'includes/RevisionList.php', 'RSSFeed' => 'includes/Feed.php', 'Sanitizer' => 'includes/Sanitizer.php', 'SiteConfiguration' => 'includes/SiteConfiguration.php', @@ -218,64 +198,88 @@ $wgAutoloadLocalClasses = array( 'SiteStatsInit' => 'includes/SiteStats.php', 'SiteStatsUpdate' => 'includes/SiteStats.php', 'Skin' => 'includes/Skin.php', + 'SkinLegacy' => 'includes/SkinLegacy.php', 'SkinTemplate' => 'includes/SkinTemplate.php', + 'SpecialCreateAccount' => 'includes/SpecialPage.php', + 'SpecialListAdmins' => 'includes/SpecialPage.php', + 'SpecialListBots' => 'includes/SpecialPage.php', 'SpecialMycontributions' => 'includes/SpecialPage.php', 'SpecialMypage' => 'includes/SpecialPage.php', 'SpecialMytalk' => 'includes/SpecialPage.php', + 'SpecialMyuploads' => 'includes/SpecialPage.php', 'SpecialPage' => 'includes/SpecialPage.php', + 'SpecialPageFactory' => 'includes/SpecialPageFactory.php', 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php', - 'SqlBagOStuff' => 'includes/BagOStuff.php', - 'SquidUpdate' => 'includes/SquidUpdate.php', 'SquidPurgeClient' => 'includes/SquidPurgeClient.php', 'SquidPurgeClientPool' => 'includes/SquidPurgeClient.php', 'Status' => 'includes/Status.php', + 'StringUtils' => 'includes/StringUtils.php', 'StubContLang' => 'includes/StubObject.php', - 'StubUserLang' => 'includes/StubObject.php', 'StubObject' => 'includes/StubObject.php', - 'StringUtils' => 'includes/StringUtils.php', + 'StubUserLang' => 'includes/StubObject.php', 'TablePager' => 'includes/Pager.php', - 'TitleDependency' => 'includes/CacheDependency.php', 'Title' => 'includes/Title.php', 'TitleArray' => 'includes/TitleArray.php', 'TitleArrayFromResult' => 'includes/TitleArray.php', - 'TitleListDependency' => 'includes/CacheDependency.php', + 'ThrottledError' => 'includes/Exception.php', 'UnlistedSpecialPage' => 'includes/SpecialPage.php', 'UppercaseCollation' => 'includes/Collation.php', 'User' => 'includes/User.php', 'UserArray' => 'includes/UserArray.php', 'UserArrayFromResult' => 'includes/UserArray.php', + 'UserBlockedError' => 'includes/Exception.php', 'UserMailer' => 'includes/UserMailer.php', 'UserRightsProxy' => 'includes/UserRightsProxy.php', + 'ViewCountUpdate' => 'includes/ViewCountUpdate.php', 'WantedQueryPage' => 'includes/QueryPage.php', 'WatchedItem' => 'includes/WatchedItem.php', - 'WatchlistEditor' => 'includes/WatchlistEditor.php', 'WebRequest' => 'includes/WebRequest.php', 'WebRequestUpload' => 'includes/WebRequest.php', 'WebResponse' => 'includes/WebResponse.php', + 'WikiCategoryPage' => 'includes/WikiCategoryPage.php', 'WikiError' => 'includes/WikiError.php', 'WikiErrorMsg' => 'includes/WikiError.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', 'WikiXmlError' => 'includes/WikiError.php', - 'WinCacheBagOStuff' => 'includes/BagOStuff.php', - 'XCacheBagOStuff' => 'includes/BagOStuff.php', - 'XmlDumpWriter' => 'includes/Export.php', 'Xml' => 'includes/Xml.php', + 'XmlDumpWriter' => 'includes/Export.php', 'XmlJsCode' => 'includes/Xml.php', 'XmlSelect' => 'includes/Xml.php', 'XmlTypeCheck' => 'includes/XmlTypeCheck.php', 'ZhClient' => 'includes/ZhClient.php', + 'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php', + + # includes/actions + 'CreditsAction' => 'includes/actions/CreditsAction.php', + 'DeletetrackbackAction' => 'includes/actions/DeletetrackbackAction.php', + 'InfoAction' => 'includes/actions/InfoAction.php', + 'MarkpatrolledAction' => 'includes/actions/MarkpatrolledAction.php', + 'PurgeAction' => 'includes/actions/PurgeAction.php', + 'RevertAction' => 'includes/actions/RevertAction.php', + 'RevertFileAction' => 'includes/actions/RevertAction.php', + 'RevisiondeleteAction' => 'includes/actions/RevisiondeleteAction.php', + 'RollbackAction' => 'includes/actions/RollbackAction.php', + 'UnwatchAction' => 'includes/actions/WatchAction.php', + 'WatchAction' => 'includes/actions/WatchAction.php', # includes/api 'ApiBase' => 'includes/api/ApiBase.php', 'ApiBlock' => 'includes/api/ApiBlock.php', + 'ApiComparePages' => 'includes/api/ApiComparePages.php', 'ApiDelete' => 'includes/api/ApiDelete.php', 'ApiDisabled' => 'includes/api/ApiDisabled.php', 'ApiEditPage' => 'includes/api/ApiEditPage.php', 'ApiEmailUser' => 'includes/api/ApiEmailUser.php', 'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php', + 'ApiFeedContributions' => 'includes/api/ApiFeedContributions.php', 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', + 'ApiFileRevert' => 'includes/api/ApiFileRevert.php', 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php', 'ApiFormatDump' => 'includes/api/ApiFormatDump.php', @@ -286,6 +290,7 @@ $wgAutoloadLocalClasses = array( 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php', 'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php', 'ApiFormatXml' => 'includes/api/ApiFormatXml.php', + 'ApiFormatXmlRsd' => 'includes/api/ApiRsd.php', 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php', 'ApiHelp' => 'includes/api/ApiHelp.php', 'ApiImport' => 'includes/api/ApiImport.php', @@ -301,14 +306,13 @@ $wgAutoloadLocalClasses = array( 'ApiPatrol' => 'includes/api/ApiPatrol.php', 'ApiProtect' => 'includes/api/ApiProtect.php', 'ApiPurge' => 'includes/api/ApiPurge.php', - 'ApiRsd' => 'includes/api/ApiRsd.php', 'ApiQuery' => 'includes/api/ApiQuery.php', 'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php', 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php', 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php', - 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php', 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php', 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php', + 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php', 'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php', 'ApiQueryBase' => 'includes/api/ApiQueryBase.php', 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php', @@ -319,20 +323,22 @@ $wgAutoloadLocalClasses = array( 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php', 'ApiQueryDisabled' => 'includes/api/ApiQueryDisabled.php', 'ApiQueryDuplicateFiles' => 'includes/api/ApiQueryDuplicateFiles.php', + 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php', 'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php', 'ApiQueryFilearchive' => 'includes/api/ApiQueryFilearchive.php', - 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php', 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php', 'ApiQueryImageInfo' => 'includes/api/ApiQueryImageInfo.php', 'ApiQueryImages' => 'includes/api/ApiQueryImages.php', 'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php', - 'ApiQueryIWLinks' => 'includes/api/ApiQueryIWLinks.php', 'ApiQueryIWBacklinks' => 'includes/api/ApiQueryIWBacklinks.php', + 'ApiQueryIWLinks' => 'includes/api/ApiQueryIWLinks.php', + 'ApiQueryLangBacklinks' => 'includes/api/ApiQueryLangBacklinks.php', 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php', 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php', 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php', 'ApiQueryPageProps' => 'includes/api/ApiQueryPageProps.php', 'ApiQueryProtectedTitles' => 'includes/api/ApiQueryProtectedTitles.php', + 'ApiQueryQueryPage' => 'includes/api/ApiQueryQueryPage.php', 'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php', 'ApiQueryRecentChanges' => 'includes/api/ApiQueryRecentChanges.php', 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', @@ -346,29 +352,37 @@ $wgAutoloadLocalClasses = array( 'ApiQueryWatchlistRaw' => 'includes/api/ApiQueryWatchlistRaw.php', 'ApiResult' => 'includes/api/ApiResult.php', 'ApiRollback' => 'includes/api/ApiRollback.php', + 'ApiRsd' => 'includes/api/ApiRsd.php', 'ApiUnblock' => 'includes/api/ApiUnblock.php', 'ApiUndelete' => 'includes/api/ApiUndelete.php', - 'ApiUserrights' => 'includes/api/ApiUserrights.php', 'ApiUpload' => 'includes/api/ApiUpload.php', + 'ApiUserrights' => 'includes/api/ApiUserrights.php', 'ApiWatch' => 'includes/api/ApiWatch.php', - 'UsageException' => 'includes/api/ApiMain.php', - # includes/extauth - 'ExternalUser_Hardcoded' => 'includes/extauth/Hardcoded.php', - 'ExternalUser_MediaWiki' => 'includes/extauth/MediaWiki.php', - 'ExternalUser_vB' => 'includes/extauth/vB.php', - - # includes/json - 'Services_JSON' => 'includes/json/Services_JSON.php', - 'Services_JSON_Error' => 'includes/json/Services_JSON.php', - 'FormatJson' => 'includes/json/FormatJson.php', + # includes/cache + 'CacheDependency' => 'includes/cache/CacheDependency.php', + 'ConstantDependency' => 'includes/cache/CacheDependency.php', + 'DependencyWrapper' => 'includes/cache/CacheDependency.php', + 'FileDependency' => 'includes/cache/CacheDependency.php', + 'GlobalDependency' => 'includes/cache/CacheDependency.php', + 'HTMLCacheUpdate' => 'includes/cache/HTMLCacheUpdate.php', + 'HTMLCacheUpdateJob' => 'includes/cache/HTMLCacheUpdate.php', + 'HTMLFileCache' => 'includes/cache/HTMLFileCache.php', + 'LinkBatch' => 'includes/cache/LinkBatch.php', + 'LinkCache' => 'includes/cache/LinkCache.php', + 'MessageCache' => 'includes/cache/MessageCache.php', + 'SquidUpdate' => 'includes/cache/SquidUpdate.php', + 'TitleDependency' => 'includes/cache/CacheDependency.php', + 'TitleListDependency' => 'includes/cache/CacheDependency.php', # includes/db - 'Blob' => 'includes/db/Database.php', + 'Blob' => 'includes/db/DatabaseUtility.php', 'ChronologyProtector' => 'includes/db/LBFactory.php', + 'CloneDatabase' => 'includes/db/CloneDatabase.php', 'Database' => 'includes/db/DatabaseMysql.php', 'DatabaseBase' => 'includes/db/Database.php', + 'DatabaseIbm_db2' => 'includes/db/DatabaseIbm_db2.php', 'DatabaseMssql' => 'includes/db/DatabaseMssql.php', 'DatabaseMysql' => 'includes/db/DatabaseMysql.php', 'DatabaseOracle' => 'includes/db/DatabaseOracle.php', @@ -376,51 +390,57 @@ $wgAutoloadLocalClasses = array( 'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php', 'DatabaseSqliteStandalone' => 'includes/db/DatabaseSqlite.php', 'DatabaseType' => 'includes/db/Database.php', - 'DBConnectionError' => 'includes/db/Database.php', - 'DBError' => 'includes/db/Database.php', - 'DBObject' => 'includes/db/Database.php', - 'DBQueryError' => 'includes/db/Database.php', - 'DBUnexpectedError' => 'includes/db/Database.php', - 'FakeResultWrapper' => 'includes/db/Database.php', - 'Field' => 'includes/db/Database.php', + 'DBConnectionError' => 'includes/db/DatabaseError.php', + 'DBError' => 'includes/db/DatabaseError.php', + 'DBObject' => 'includes/db/DatabaseUtility.php', + 'DBMasterPos' => 'includes/db/DatabaseUtility.php', + 'DBQueryError' => 'includes/db/DatabaseError.php', + 'DBUnexpectedError' => 'includes/db/DatabaseError.php', + 'FakeResultWrapper' => 'includes/db/DatabaseUtility.php', + 'Field' => 'includes/db/DatabaseUtility.php', 'IBM_DB2Blob' => 'includes/db/DatabaseIbm_db2.php', + 'IBM_DB2Field' => 'includes/db/DatabaseIbm_db2.php', 'LBFactory' => 'includes/db/LBFactory.php', 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php', 'LBFactory_Simple' => 'includes/db/LBFactory.php', 'LBFactory_Single' => 'includes/db/LBFactory_Single.php', - 'LikeMatch' => 'includes/db/Database.php', + 'LikeMatch' => 'includes/db/DatabaseUtility.php', 'LoadBalancer' => 'includes/db/LoadBalancer.php', 'LoadBalancer_Single' => 'includes/db/LBFactory_Single.php', 'LoadMonitor' => 'includes/db/LoadMonitor.php', 'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php', + 'LoadMonitor_Null' => 'includes/db/LoadMonitor.php', 'MySQLField' => 'includes/db/DatabaseMysql.php', 'MySQLMasterPos' => 'includes/db/DatabaseMysql.php', 'ORAField' => 'includes/db/DatabaseOracle.php', 'ORAResult' => 'includes/db/DatabaseOracle.php', 'PostgresField' => 'includes/db/DatabasePostgres.php', - 'ResultWrapper' => 'includes/db/Database.php', + 'ResultWrapper' => 'includes/db/DatabaseUtility.php', 'SQLiteField' => 'includes/db/DatabaseSqlite.php', - 'DatabaseIbm_db2' => 'includes/db/DatabaseIbm_db2.php', - 'IBM_DB2Field' => 'includes/db/DatabaseIbm_db2.php', # includes/diff - 'ArrayDiffFormatter' => 'includes/diff/WikiDiff.php', - '_DiffEngine' => 'includes/diff/WikiDiff.php', + '_DiffEngine' => 'includes/diff/DairikiDiff.php', + '_DiffOp' => 'includes/diff/DairikiDiff.php', + '_DiffOp_Add' => 'includes/diff/DairikiDiff.php', + '_DiffOp_Change' => 'includes/diff/DairikiDiff.php', + '_DiffOp_Copy' => 'includes/diff/DairikiDiff.php', + '_DiffOp_Delete' => 'includes/diff/DairikiDiff.php', + '_HWLDF_WordAccumulator' => 'includes/diff/DairikiDiff.php', + 'ArrayDiffFormatter' => 'includes/diff/DairikiDiff.php', + 'Diff' => 'includes/diff/DairikiDiff.php', 'DifferenceEngine' => 'includes/diff/DifferenceEngine.php', - 'DiffFormatter' => 'includes/diff/WikiDiff.php', - 'Diff' => 'includes/diff/WikiDiff.php', - '_DiffOp_Add' => 'includes/diff/WikiDiff.php', - '_DiffOp_Change' => 'includes/diff/WikiDiff.php', - '_DiffOp_Copy' => 'includes/diff/WikiDiff.php', - '_DiffOp_Delete' => 'includes/diff/WikiDiff.php', - '_DiffOp' => 'includes/diff/WikiDiff.php', - '_HWLDF_WordAccumulator' => 'includes/diff/WikiDiff.php', - 'MappedDiff' => 'includes/diff/WikiDiff.php', + 'DiffFormatter' => 'includes/diff/DairikiDiff.php', + 'MappedDiff' => 'includes/diff/DairikiDiff.php', 'RangeDifference' => 'includes/diff/WikiDiff3.php', - 'TableDiffFormatter' => 'includes/diff/WikiDiff.php', - 'UnifiedDiffFormatter' => 'includes/diff/WikiDiff.php', + 'TableDiffFormatter' => 'includes/diff/DairikiDiff.php', + 'UnifiedDiffFormatter' => 'includes/diff/DairikiDiff.php', 'WikiDiff3' => 'includes/diff/WikiDiff3.php', - 'WordLevelDiff' => 'includes/diff/WikiDiff.php', + 'WordLevelDiff' => 'includes/diff/DairikiDiff.php', + + # includes/extauth + 'ExternalUser_Hardcoded' => 'includes/extauth/Hardcoded.php', + 'ExternalUser_MediaWiki' => 'includes/extauth/MediaWiki.php', + 'ExternalUser_vB' => 'includes/extauth/vB.php', # includes/filerepo 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', @@ -433,7 +453,6 @@ $wgAutoloadLocalClasses = array( 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php', 'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php', 'FSRepo' => 'includes/filerepo/FSRepo.php', - 'Image' => 'includes/filerepo/Image.php', 'LocalFile' => 'includes/filerepo/LocalFile.php', 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php', 'LocalFileMoveBatch' => 'includes/filerepo/LocalFile.php', @@ -445,24 +464,43 @@ $wgAutoloadLocalClasses = array( # includes/installer 'CliInstaller' => 'includes/installer/CliInstaller.php', - 'Installer' => 'includes/installer/Installer.php', 'DatabaseInstaller' => 'includes/installer/DatabaseInstaller.php', 'DatabaseUpdater' => 'includes/installer/DatabaseUpdater.php', + 'Ibm_db2Installer' => 'includes/installer/Ibm_db2Installer.php', + 'Ibm_db2Updater' => 'includes/installer/Ibm_db2Updater.php', + 'InstallDocFormatter' => 'includes/installer/InstallDocFormatter.php', + 'Installer' => 'includes/installer/Installer.php', 'LBFactory_InstallerFake' => 'includes/installer/Installer.php', 'LocalSettingsGenerator' => 'includes/installer/LocalSettingsGenerator.php', - 'WebInstaller' => 'includes/installer/WebInstaller.php', - 'WebInstallerPage' => 'includes/installer/WebInstallerPage.php', - 'WebInstallerOutput' => 'includes/installer/WebInstallerOutput.php', 'MysqlInstaller' => 'includes/installer/MysqlInstaller.php', 'MysqlUpdater' => 'includes/installer/MysqlUpdater.php', - 'PhpXmlBugTester' => 'includes/installer/PhpBugTests.php', + 'OracleInstaller' => 'includes/installer/OracleInstaller.php', + 'OracleUpdater' => 'includes/installer/OracleUpdater.php', 'PhpRefCallBugTester' => 'includes/installer/PhpBugTests.php', + 'PhpXmlBugTester' => 'includes/installer/PhpBugTests.php', 'PostgresInstaller' => 'includes/installer/PostgresInstaller.php', 'PostgresUpdater' => 'includes/installer/PostgresUpdater.php', 'SqliteInstaller' => 'includes/installer/SqliteInstaller.php', 'SqliteUpdater' => 'includes/installer/SqliteUpdater.php', - 'OracleInstaller' => 'includes/installer/OracleInstaller.php', - 'OracleUpdater' => 'includes/installer/OracleUpdater.php', + 'WebInstaller' => 'includes/installer/WebInstaller.php', + 'WebInstaller_Complete' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Copying' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_DBConnect' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_DBSettings' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Document' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_ExistingWiki' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Install' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Language' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Name' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Options' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Readme' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_ReleaseNotes' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Restart' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Upgrade' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_UpgradeDoc' => 'includes/installer/WebInstallerPage.php', + 'WebInstaller_Welcome' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerOutput' => 'includes/installer/WebInstallerOutput.php', + 'WebInstallerPage' => 'includes/installer/WebInstallerPage.php', # includes/job 'DoubleRedirectJob' => 'includes/job/DoubleRedirectJob.php', @@ -473,19 +511,37 @@ $wgAutoloadLocalClasses = array( 'RefreshLinksJob2' => 'includes/job/RefreshLinksJob.php', 'UploadFromUrlJob' => 'includes/job/UploadFromUrlJob.php', + # includes/json + 'FormatJson' => 'includes/json/FormatJson.php', + 'Services_JSON' => 'includes/json/Services_JSON.php', + 'Services_JSON_Error' => 'includes/json/Services_JSON.php', + # includes/libs + 'CSSJanus' => 'includes/libs/CSSJanus.php', + 'CSSMin' => 'includes/libs/CSSMin.php', + 'HttpStatus' => 'includes/libs/HttpStatus.php', 'IEContentAnalyzer' => 'includes/libs/IEContentAnalyzer.php', 'IEUrlExtension' => 'includes/libs/IEUrlExtension.php', - 'Spyc' => 'includes/libs/spyc.php', + 'JavaScriptMinifier' => 'includes/libs/JavaScriptMinifier.php', + 'JSMinPlus' => 'includes/libs/jsminplus.php', + 'JSParser' => 'includes/libs/jsminplus.php', # includes/media 'BitmapHandler' => 'includes/media/Bitmap.php', 'BitmapHandler_ClientOnly' => 'includes/media/Bitmap_ClientOnly.php', + 'BitmapMetadataHandler' => 'includes/media/BitmapMetadataHandler.php', 'BmpHandler' => 'includes/media/BMP.php', 'DjVuHandler' => 'includes/media/DjVu.php', + 'Exif' => 'includes/media/Exif.php', + 'FormatExif' => 'includes/media/FormatMetadata.php', + 'FormatMetadata' => 'includes/media/FormatMetadata.php', 'GIFHandler' => 'includes/media/GIF.php', 'GIFMetadataExtractor' => 'includes/media/GIFMetadataExtractor.php', 'ImageHandler' => 'includes/media/Generic.php', + 'IPTC' => 'includes/media/IPTC.php', + 'JpegHandler' => 'includes/media/Jpeg.php', + 'JpegMetadataExtractor' => 'includes/media/JpegMetadataExtractor.php', + 'ExifBitmapHandler' => 'includes/media/ExifBitmap.php', 'MediaHandler' => 'includes/media/Generic.php', 'MediaTransformError' => 'includes/media/MediaTransformOutput.php', 'MediaTransformOutput' => 'includes/media/MediaTransformOutput.php', @@ -496,38 +552,72 @@ $wgAutoloadLocalClasses = array( 'ThumbnailImage' => 'includes/media/MediaTransformOutput.php', 'TiffHandler' => 'includes/media/Tiff.php', 'TransformParameterError' => 'includes/media/MediaTransformOutput.php', + 'XMPInfo' => 'includes/media/XMPInfo.php', + 'XMPReader' => 'includes/media/XMP.php', + 'XMPValidate' => 'includes/media/XMPValidate.php', # includes/normal 'UtfNormal' => 'includes/normal/UtfNormal.php', + # includes/objectcache + 'APCBagOStuff' => 'includes/objectcache/APCBagOStuff.php', + 'BagOStuff' => 'includes/objectcache/BagOStuff.php', + 'DBABagOStuff' => 'includes/objectcache/DBABagOStuff.php', + 'eAccelBagOStuff' => 'includes/objectcache/eAccelBagOStuff.php', + 'EhcacheBagOStuff' => 'includes/objectcache/EhcacheBagOStuff.php', + 'EmptyBagOStuff' => 'includes/objectcache/EmptyBagOStuff.php', + 'FakeMemCachedClient' => 'includes/objectcache/EmptyBagOStuff.php', + 'HashBagOStuff' => 'includes/objectcache/HashBagOStuff.php', + 'MediaWikiBagOStuff' => 'includes/objectcache/SqlBagOStuff.php', + 'MemCachedClientforWiki' => 'includes/objectcache/MemcachedClient.php', + 'MemcachedPhpBagOStuff' => 'includes/objectcache/MemcachedPhpBagOStuff.php', + 'MultiWriteBagOStuff' => 'includes/objectcache/MultiWriteBagOStuff.php', + 'MWMemcached' => 'includes/objectcache/MemcachedClient.php', + 'ObjectCache' => 'includes/objectcache/ObjectCache.php', + 'SqlBagOStuff' => 'includes/objectcache/SqlBagOStuff.php', + 'WinCacheBagOStuff' => 'includes/objectcache/WinCacheBagOStuff.php', + 'XCacheBagOStuff' => 'includes/objectcache/XCacheBagOStuff.php', + # includes/parser + 'CacheTime' => 'includes/parser/ParserOutput.php', 'CoreLinkFunctions' => 'includes/parser/CoreLinkFunctions.php', 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php', 'CoreTagHooks' => 'includes/parser/CoreTagHooks.php', 'DateFormatter' => 'includes/parser/DateFormatter.php', 'LinkHolderArray' => 'includes/parser/LinkHolderArray.php', 'LinkMarkerReplacer' => 'includes/parser/Parser_LinkHooks.php', - 'OnlyIncludeReplacer' => 'includes/parser/Parser.php', - 'PPCustomFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'MWTidy' => 'includes/parser/Tidy.php', 'PPCustomFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', + 'PPCustomFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPCustomFrame_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp', 'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDAccum_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp', 'PPDPart' => 'includes/parser/Preprocessor_DOM.php', 'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDPart_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp', 'PPDStack' => 'includes/parser/Preprocessor_DOM.php', 'PPDStackElement' => 'includes/parser/Preprocessor_DOM.php', 'PPDStackElement_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDStackElement_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp', 'PPDStack_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDStack_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp', 'PPFrame' => 'includes/parser/Preprocessor.php', 'PPFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', 'PPFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPFrame_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp', 'PPNode' => 'includes/parser/Preprocessor.php', 'PPNode_DOM' => 'includes/parser/Preprocessor_DOM.php', 'PPNode_Hash_Array' => 'includes/parser/Preprocessor_Hash.php', 'PPNode_Hash_Attr' => 'includes/parser/Preprocessor_Hash.php', 'PPNode_Hash_Text' => 'includes/parser/Preprocessor_Hash.php', 'PPNode_Hash_Tree' => 'includes/parser/Preprocessor_Hash.php', + 'PPNode_HipHop_Array' => 'includes/parser/Preprocessor_HipHop.hphp', + 'PPNode_HipHop_Attr' => 'includes/parser/Preprocessor_HipHop.hphp', + 'PPNode_HipHop_Text' => 'includes/parser/Preprocessor_HipHop.hphp', + 'PPNode_HipHop_Tree' => 'includes/parser/Preprocessor_HipHop.hphp', 'PPTemplateFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', 'PPTemplateFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPTemplateFrame_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp', 'Parser' => 'includes/parser/Parser.php', 'ParserCache' => 'includes/parser/ParserCache.php', 'ParserOptions' => 'includes/parser/ParserOptions.php', @@ -537,15 +627,55 @@ $wgAutoloadLocalClasses = array( 'Preprocessor' => 'includes/parser/Preprocessor.php', 'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php', 'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php', - 'StripState' => 'includes/parser/Parser.php', - 'MWTidy' => 'includes/parser/Tidy.php', + 'Preprocessor_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp', + 'StripState' => 'includes/parser/StripState.php', + + # includes/profiler + 'Profiler' => 'includes/profiler/Profiler.php', + 'ProfilerSimple' => 'includes/profiler/ProfilerSimple.php', + 'ProfilerSimpleText' => 'includes/profiler/ProfilerSimpleText.php', + 'ProfilerSimpleTrace' => 'includes/profiler/ProfilerSimpleTrace.php', + 'ProfilerSimpleUDP' => 'includes/profiler/ProfilerSimpleUDP.php', + 'ProfilerStub' => 'includes/profiler/ProfilerStub.php', + + # includes/resourceloader + 'ResourceLoader' => 'includes/resourceloader/ResourceLoader.php', + 'ResourceLoaderContext' => 'includes/resourceloader/ResourceLoaderContext.php', + 'ResourceLoaderFileModule' => 'includes/resourceloader/ResourceLoaderFileModule.php', + 'ResourceLoaderFilePageModule' => 'includes/resourceloader/ResourceLoaderFilePageModule.php', + 'ResourceLoaderModule' => 'includes/resourceloader/ResourceLoaderModule.php', + 'ResourceLoaderNoscriptModule' => 'includes/resourceloader/ResourceLoaderNoscriptModule.php', + 'ResourceLoaderSiteModule' => 'includes/resourceloader/ResourceLoaderSiteModule.php', + 'ResourceLoaderStartUpModule' => 'includes/resourceloader/ResourceLoaderStartUpModule.php', + 'ResourceLoaderUserGroupsModule' => 'includes/resourceloader/ResourceLoaderUserGroupsModule.php', + 'ResourceLoaderUserModule' => 'includes/resourceloader/ResourceLoaderUserModule.php', + 'ResourceLoaderUserOptionsModule' => 'includes/resourceloader/ResourceLoaderUserOptionsModule.php', + 'ResourceLoaderUserTokensModule' => 'includes/resourceloader/ResourceLoaderUserTokensModule.php', + 'ResourceLoaderWikiModule' => 'includes/resourceloader/ResourceLoaderWikiModule.php', + + # includes/revisiondelete + 'RevDel_ArchivedFileItem' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_ArchivedFileList' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_ArchiveItem' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_ArchiveList' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_FileItem' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_FileList' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_Item' => 'includes/revisiondelete/RevisionDeleteAbstracts.php', + 'RevDel_List' => 'includes/revisiondelete/RevisionDeleteAbstracts.php', + 'RevDel_LogItem' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_LogList' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_RevisionItem' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_RevisionList' => 'includes/revisiondelete/RevisionDelete.php', + 'RevisionDelete' => 'includes/revisiondelete/RevisionDelete.php', + 'RevisionDeleter' => 'includes/revisiondelete/RevisionDeleter.php', + 'RevisionDeleteUser' => 'includes/revisiondelete/RevisionDeleteUser.php', # includes/search 'MySQLSearchResultSet' => 'includes/search/SearchMySQL.php', 'PostgresSearchResult' => 'includes/search/SearchPostgres.php', 'PostgresSearchResultSet' => 'includes/search/SearchPostgres.php', - 'SearchEngineDummy' => 'includes/search/SearchEngine.php', 'SearchEngine' => 'includes/search/SearchEngine.php', + 'SearchEngineDummy' => 'includes/search/SearchEngine.php', 'SearchHighlighter' => 'includes/search/SearchEngine.php', 'SearchIBM_DB2' => 'includes/search/SearchIBM_DB2.php', 'SearchMssql' => 'includes/search/SearchMssql.php', @@ -562,30 +692,26 @@ $wgAutoloadLocalClasses = array( 'SqlSearchResultSet' => 'includes/search/SearchEngine.php', # includes/specials - 'SpecialAllmessages' => 'includes/specials/SpecialAllmessages.php', 'ActiveUsersPager' => 'includes/specials/SpecialActiveusers.php', 'AllmessagesTablePager' => 'includes/specials/SpecialAllmessages.php', 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php', + 'BlockListPager' => 'includes/specials/SpecialBlockList.php', 'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php', 'ContribsPager' => 'includes/specials/SpecialContributions.php', 'DBLockForm' => 'includes/specials/SpecialLockdb.php', 'DBUnlockForm' => 'includes/specials/SpecialUnlockdb.php', 'DeadendPagesPage' => 'includes/specials/SpecialDeadendpages.php', - 'DeletedContributionsPage' => 'includes/specials/SpecialDeletedContributions.php', 'DeletedContribsPager' => 'includes/specials/SpecialDeletedContributions.php', + 'DeletedContributionsPage' => 'includes/specials/SpecialDeletedContributions.php', 'DisambiguationsPage' => 'includes/specials/SpecialDisambiguations.php', 'DoubleRedirectsPage' => 'includes/specials/SpecialDoubleRedirects.php', 'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php', 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php', - 'SpecialEmailUser' => 'includes/specials/SpecialEmailuser.php', 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php', 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php', - 'IPBlockForm' => 'includes/specials/SpecialBlockip.php', - 'IPBlocklistPager' => 'includes/specials/SpecialIpblocklist.php', - 'IPUnblockForm' => 'includes/specials/SpecialIpblocklist.php', + 'HTMLBlockedUsersItemSelect' => 'includes/specials/SpecialBlockList.php', 'ImportReporter' => 'includes/specials/SpecialImport.php', - 'ImportStreamSource' => 'includes/Import.php', - 'ImportStringSource' => 'includes/Import.php', + 'IPBlockForm' => 'includes/specials/SpecialBlock.php', 'LinkSearchPage' => 'includes/specials/SpecialLinkSearch.php', 'ListredirectsPage' => 'includes/specials/SpecialListredirects.php', 'LoginForm' => 'includes/specials/SpecialUserlogin.php', @@ -596,46 +722,42 @@ $wgAutoloadLocalClasses = array( 'MostimagesPage' => 'includes/specials/SpecialMostimages.php', 'MostlinkedCategoriesPage' => 'includes/specials/SpecialMostlinkedcategories.php', 'MostlinkedPage' => 'includes/specials/SpecialMostlinked.php', + 'MostlinkedTemplatesPage' => 'includes/specials/SpecialMostlinkedtemplates.php', 'MostrevisionsPage' => 'includes/specials/SpecialMostrevisions.php', 'MovePageForm' => 'includes/specials/SpecialMovepage.php', - 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php', - 'SpecialContributions' => 'includes/specials/SpecialContributions.php', 'NewPagesPager' => 'includes/specials/SpecialNewpages.php', 'PageArchive' => 'includes/specials/SpecialUndelete.php', - 'SpecialResetpass' => 'includes/specials/SpecialResetpass.php', 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php', - 'PreferencesForm' => 'includes/Preferences.php', 'RandomPage' => 'includes/specials/SpecialRandompage.php', - 'SpecialRevisionDelete' => 'includes/specials/SpecialRevisiondelete.php', - 'RevisionDeleter' => 'includes/revisiondelete/RevisionDeleter.php', - 'RevDel_List' => 'includes/revisiondelete/RevisionDeleteAbstracts.php', - 'RevDel_Item' => 'includes/revisiondelete/RevisionDeleteAbstracts.php', - 'RevDel_RevisionList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_RevisionItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_ArchiveList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_ArchiveItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_FileList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_FileItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_ArchivedFileList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_ArchivedFileItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_LogList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_LogItem' => 'includes/revisiondelete/RevisionDelete.php', 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php', 'SpecialActiveUsers' => 'includes/specials/SpecialActiveusers.php', + 'SpecialAllmessages' => 'includes/specials/SpecialAllmessages.php', 'SpecialAllpages' => 'includes/specials/SpecialAllpages.php', 'SpecialBlankpage' => 'includes/specials/SpecialBlankpage.php', + 'SpecialBlock' => 'includes/specials/SpecialBlock.php', + 'SpecialBlockList' => 'includes/specials/SpecialBlockList.php', 'SpecialBlockme' => 'includes/specials/SpecialBlockme.php', 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php', 'SpecialCategories' => 'includes/specials/SpecialCategories.php', + 'SpecialChangePassword' => 'includes/specials/SpecialChangePassword.php', 'SpecialComparePages' => 'includes/specials/SpecialComparePages.php', + 'SpecialContributions' => 'includes/specials/SpecialContributions.php', + 'SpecialEditWatchlist' => 'includes/specials/SpecialEditWatchlist.php', + 'SpecialEmailUser' => 'includes/specials/SpecialEmailuser.php', 'SpecialExport' => 'includes/specials/SpecialExport.php', 'SpecialFilepath' => 'includes/specials/SpecialFilepath.php', 'SpecialImport' => 'includes/specials/SpecialImport.php', + 'SpecialListFiles' => 'includes/specials/SpecialListfiles.php', 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php', + 'SpecialListUsers' => 'includes/specials/SpecialListusers.php', 'SpecialLockdb' => 'includes/specials/SpecialLockdb.php', 'SpecialLog' => 'includes/specials/SpecialLog.php', 'SpecialMergeHistory' => 'includes/specials/SpecialMergeHistory.php', 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php', + 'SpecialNewFiles' => 'includes/specials/SpecialNewimages.php', + 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php', + 'SpecialPasswordReset' => 'includes/specials/SpecialPasswordReset.php', + 'SpecialPermanentLink' => 'includes/SpecialPage.php', 'SpecialPreferences' => 'includes/specials/SpecialPreferences.php', 'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php', 'SpecialProtectedpages' => 'includes/specials/SpecialProtectedpages.php', @@ -643,21 +765,24 @@ $wgAutoloadLocalClasses = array( 'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php', 'SpecialRecentChanges' => 'includes/specials/SpecialRecentchanges.php', 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php', + 'SpecialRevisionDelete' => 'includes/specials/SpecialRevisiondelete.php', 'SpecialSearch' => 'includes/specials/SpecialSearch.php', - 'SpecialUploadStash' => 'includes/specials/SpecialUploadStash.php', 'SpecialSpecialpages' => 'includes/specials/SpecialSpecialpages.php', 'SpecialStatistics' => 'includes/specials/SpecialStatistics.php', 'SpecialTags' => 'includes/specials/SpecialTags.php', + 'SpecialUnblock' => 'includes/specials/SpecialUnblock.php', + 'SpecialUndelete' => 'includes/specials/SpecialUndelete.php', 'SpecialUnlockdb' => 'includes/specials/SpecialUnlockdb.php', 'SpecialUpload' => 'includes/specials/SpecialUpload.php', + 'SpecialUploadStash' => 'includes/specials/SpecialUploadStash.php', 'SpecialUserlogout' => 'includes/specials/SpecialUserlogout.php', 'SpecialVersion' => 'includes/specials/SpecialVersion.php', + 'SpecialWatchlist' => 'includes/specials/SpecialWatchlist.php', 'SpecialWhatlinkshere' => 'includes/specials/SpecialWhatlinkshere.php', - 'SpecialWhatLinksHere' => 'includes/specials/SpecialWhatlinkshere.php', 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php', + 'UncategorizedImagesPage' => 'includes/specials/SpecialUncategorizedimages.php', 'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php', 'UncategorizedTemplatesPage' => 'includes/specials/SpecialUncategorizedtemplates.php', - 'UndeleteForm' => 'includes/specials/SpecialUndelete.php', 'UnusedCategoriesPage' => 'includes/specials/SpecialUnusedcategories.php', 'UnusedimagesPage' => 'includes/specials/SpecialUnusedimages.php', 'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php', @@ -670,9 +795,7 @@ $wgAutoloadLocalClasses = array( 'WantedFilesPage' => 'includes/specials/SpecialWantedfiles.php', 'WantedPagesPage' => 'includes/specials/SpecialWantedpages.php', 'WantedTemplatesPage' => 'includes/specials/SpecialWantedtemplates.php', - 'WhatLinksHerePage' => 'includes/specials/SpecialWhatlinkshere.php', - 'WikiImporter' => 'includes/Import.php', - 'WikiRevision' => 'includes/Import.php', + 'WatchlistEditor' => 'includes/specials/SpecialEditWatchlist.php', 'WithoutInterwikiPage' => 'includes/specials/SpecialWithoutinterwiki.php', # includes/templates @@ -681,66 +804,71 @@ $wgAutoloadLocalClasses = array( # includes/upload 'UploadBase' => 'includes/upload/UploadBase.php', - 'UploadFromStash' => 'includes/upload/UploadFromStash.php', 'UploadFromFile' => 'includes/upload/UploadFromFile.php', + 'UploadFromStash' => 'includes/upload/UploadFromStash.php', 'UploadFromUrl' => 'includes/upload/UploadFromUrl.php', 'UploadStash' => 'includes/upload/UploadStash.php', - 'UploadStashFile' => 'includes/upload/UploadStash.php', - 'UploadStashNotAvailableException' => 'includes/upload/UploadStash.php', - 'UploadStashFileNotFoundException' => 'includes/upload/UploadStash.php', 'UploadStashBadPathException' => 'includes/upload/UploadStash.php', 'UploadStashBadVersionException' => 'includes/upload/UploadStash.php', + 'UploadStashFile' => 'includes/upload/UploadStash.php', 'UploadStashFileException' => 'includes/upload/UploadStash.php', + 'UploadStashFileNotFoundException' => 'includes/upload/UploadStash.php', + 'UploadStashNotAvailableException' => 'includes/upload/UploadStash.php', 'UploadStashZeroLengthFileException' => 'includes/upload/UploadStash.php', # languages - 'Language' => 'languages/Language.php', 'FakeConverter' => 'languages/Language.php', + 'Language' => 'languages/Language.php', 'LanguageConverter' => 'languages/LanguageConverter.php', # maintenance - 'AnsiTermColorer' => 'maintenance/tests/testHelpers.inc', 'ConvertLinks' => 'maintenance/convertLinks.php', - 'DbTestPreviewer' => 'maintenance/tests/testHelpers.inc', - 'DbTestRecorder' => 'maintenance/tests/testHelpers.inc', 'DeleteArchivedFilesImplementation' => 'maintenance/deleteArchivedFiles.inc', 'DeleteArchivedRevisionsImplementation' => 'maintenance/deleteArchivedRevisions.inc', 'DeleteDefaultMessages' => 'maintenance/deleteDefaultMessages.php', - 'DummyTermColorer' => 'maintenance/tests/testHelpers.inc', 'FakeMaintenance' => 'maintenance/Maintenance.php', + 'LoggedUpdateMaintenance' => 'maintenance/Maintenance.php', 'Maintenance' => 'maintenance/Maintenance.php', - 'ParserTest' => 'maintenance/tests/parser/parserTest.inc', - 'ParserTestParserHook' => 'maintenance/tests/parser/parserTestsParserHook.php', - 'ParserTestStaticParserHook' => 'maintenance/tests/parser/parserTestsStaticParserHook.php', + 'FixExtLinksProtocolRelative' => 'maintenance/fixExtLinksProtocolRelative.php', 'PopulateCategory' => 'maintenance/populateCategory.php', 'PopulateLogSearch' => 'maintenance/populateLogSearch.php', 'PopulateLogUsertext' => 'maintenance/populateLogUsertext.php', 'PopulateParentId' => 'maintenance/populateParentId.php', 'PopulateRevisionLength' => 'maintenance/populateRevisionLength.php', - 'RemoteTestRecorder' => 'maintenance/tests/testHelpers.inc', 'SevenZipStream' => 'maintenance/7zip.inc', 'Sqlite' => 'maintenance/sqlite.inc', - 'TestFileIterator' => 'maintenance/tests/testHelpers.inc', - 'TestRecorder' => 'maintenance/tests/testHelpers.inc', 'UpdateCollation' => 'maintenance/updateCollation.php', 'UpdateRestrictions' => 'maintenance/updateRestrictions.php', 'UserDupes' => 'maintenance/userDupes.inc', - # maintenance/tests/selenium - 'Selenium' => 'maintenance/tests/selenium/Selenium.php', - 'SeleniumLoader' => 'maintenance/tests/selenium/SeleniumLoader.php', - 'SeleniumTestCase' => 'maintenance/tests/selenium/SeleniumTestCase.php', - 'SeleniumTestConsoleLogger' => 'maintenance/tests/selenium/SeleniumTestConsoleLogger.php', - 'SeleniumTestHTMLLogger' => 'maintenance/tests/selenium/SeleniumTestHTMLLogger.php', - 'SeleniumTestListener' => 'maintenance/tests/selenium/SeleniumTestListener.php', - 'SeleniumTestSuite' => 'maintenance/tests/selenium/SeleniumTestSuite.php', - 'SeleniumConfig' => 'maintenance/tests/selenium/SeleniumConfig.php', - # maintenance/language 'csvStatsOutput' => 'maintenance/language/StatOutputs.php', 'statsOutput' => 'maintenance/language/StatOutputs.php', 'textStatsOutput' => 'maintenance/language/StatOutputs.php', 'wikiStatsOutput' => 'maintenance/language/StatOutputs.php', + + # tests + 'AnsiTermColorer' => 'tests/testHelpers.inc', + 'DbTestPreviewer' => 'tests/testHelpers.inc', + 'DbTestRecorder' => 'tests/testHelpers.inc', + 'DummyTermColorer' => 'tests/testHelpers.inc', + 'TestFileIterator' => 'tests/testHelpers.inc', + 'TestRecorder' => 'tests/testHelpers.inc', + + # tests/parser + 'ParserTest' => 'tests/parser/parserTest.inc', + 'ParserTestParserHook' => 'tests/parser/parserTestsParserHook.php', + 'ParserTestStaticParserHook' => 'tests/parser/parserTestsStaticParserHook.php', + + # tests/selenium + 'Selenium' => 'tests/selenium/Selenium.php', + 'SeleniumLoader' => 'tests/selenium/SeleniumLoader.php', + 'SeleniumTestCase' => 'tests/selenium/SeleniumTestCase.php', + 'SeleniumTestConsoleLogger' => 'tests/selenium/SeleniumTestConsoleLogger.php', + 'SeleniumTestHTMLLogger' => 'tests/selenium/SeleniumTestHTMLLogger.php', + 'SeleniumTestListener' => 'tests/selenium/SeleniumTestListener.php', + 'SeleniumTestSuite' => 'tests/selenium/SeleniumTestSuite.php', + 'SeleniumConfig' => 'tests/selenium/SeleniumConfig.php', ); class AutoLoader { @@ -792,16 +920,6 @@ class AutoLoader { return true; } - static function loadAllExtensions() { - global $wgAutoloadClasses; - - foreach ( $wgAutoloadClasses as $class => $file ) { - if ( !( class_exists( $class, false ) || interface_exists( $class, false ) ) ) { - require( $file ); - } - } - } - /** * Force a class to be run through the autoloader, helpful for things like * Sanitizer that have define()s outside of their class definition. Of course diff --git a/includes/Autopromote.php b/includes/Autopromote.php index b4d89b24..83f3c20b 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -8,7 +8,7 @@ class Autopromote { /** * Get the groups for the given user based on $wgAutopromote. * - * @param $user The user to get the groups for + * @param $user User The user to get the groups for * @return array Array of groups to promote to. */ public static function getAutopromoteGroups( User $user ) { @@ -28,8 +28,47 @@ class Autopromote { } /** + * Get the groups for the given user based on the given criteria. + * + * Does not return groups the user already belongs to or has once belonged. + * + * @param $user The user to get the groups for + * @param $event String key in $wgAutopromoteOnce (each one has groups/criteria) + * + * @return array Groups the user should be promoted to. + * + * @see $wgAutopromoteOnce + */ + public static function getAutopromoteOnceGroups( User $user, $event ) { + global $wgAutopromoteOnce; + + $promote = array(); + + if ( isset( $wgAutopromoteOnce[$event] ) && count( $wgAutopromoteOnce[$event] ) ) { + $currentGroups = $user->getGroups(); + $formerGroups = $user->getFormerGroups(); + foreach ( $wgAutopromoteOnce[$event] as $group => $cond ) { + // Do not check if the user's already a member + if ( in_array( $group, $currentGroups ) ) { + continue; + } + // Do not autopromote if the user has belonged to the group + if ( in_array( $group, $formerGroups ) ) { + continue; + } + // Finally - check the conditions + if ( self::recCheckCondition( $cond, $user ) ) { + $promote[] = $group; + } + } + } + + return $promote; + } + + /** * Recursively check a condition. Conditions are in the form - * array( '&' or '|' or '^', cond1, cond2, ... ) + * array( '&' or '|' or '^' or '!', cond1, cond2, ... ) * where cond1, cond2, ... are themselves conditions; *OR* * APCOND_EMAILCONFIRMED, *OR* * array( APCOND_EMAILCONFIRMED ), *OR* @@ -40,7 +79,7 @@ class Autopromote { * self::checkCondition for evaluation of the latter type. * * @param $cond Mixed: a condition, possibly containing other conditions - * @param $user The user to check the conditions against + * @param $user User The user to check the conditions against * @return bool Whether the condition is true */ private static function recCheckCondition( $cond, User $user ) { @@ -48,7 +87,7 @@ class Autopromote { if ( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) { # Recursive condition - if ( $cond[0] == '&' ) { + if ( $cond[0] == '&' ) { // AND (all conds pass) foreach ( array_slice( $cond, 1 ) as $subcond ) { if ( !self::recCheckCondition( $subcond, $user ) ) { return false; @@ -56,7 +95,7 @@ class Autopromote { } return true; - } elseif ( $cond[0] == '|' ) { + } elseif ( $cond[0] == '|' ) { // OR (at least one cond passes) foreach ( array_slice( $cond, 1 ) as $subcond ) { if ( self::recCheckCondition( $subcond, $user ) ) { return true; @@ -64,18 +103,13 @@ class Autopromote { } return false; - } elseif ( $cond[0] == '^' ) { - $res = null; - foreach ( array_slice( $cond, 1 ) as $subcond ) { - if ( is_null( $res ) ) { - $res = self::recCheckCondition( $subcond, $user ); - } else { - $res = ( $res xor self::recCheckCondition( $subcond, $user ) ); - } + } elseif ( $cond[0] == '^' ) { // XOR (exactly one cond passes) + if ( count( $cond ) > 3 ) { + wfWarn( 'recCheckCondition() given XOR ("^") condition on three or more conditions. Check your $wgAutopromote and $wgAutopromoteOnce settings.' ); } - - return $res; - } elseif ( $cond[0] == '!' ) { + return self::recCheckCondition( $cond[1], $user ) + xor self::recCheckCondition( $cond[2], $user ); + } elseif ( $cond[0] == '!' ) { // NOT (no conds pass) foreach ( array_slice( $cond, 1 ) as $subcond ) { if ( self::recCheckCondition( $subcond, $user ) ) { return false; @@ -101,7 +135,7 @@ class Autopromote { * ates them. * * @param $cond Array: A condition, which must not contain other conditions - * @param $user The user to check the condition against + * @param $user User The user to check the condition against * @return bool Whether the condition is true for the user */ private static function checkCondition( $cond, User $user ) { @@ -112,7 +146,7 @@ class Autopromote { switch( $cond[0] ) { case APCOND_EMAILCONFIRMED: - if ( User::isValidEmailAddr( $user->getEmail() ) ) { + if ( Sanitizer::validateEmail( $user->getEmail() ) ) { if ( $wgEmailAuthentication ) { return (bool)$user->getEmailAuthenticationTimestamp(); } else { @@ -137,6 +171,8 @@ class Autopromote { return IP::isInRange( wfGetIP(), $cond[1] ); case APCOND_BLOCKED: return $user->isBlocked(); + case APCOND_ISBOT: + return in_array( 'bot', User::getGroupPermissions( $user->getGroups() ) ); default: $result = null; wfRunHooks( 'AutopromoteCondition', array( $cond[0], array_slice( $cond, 1 ), $user, &$result ) ); diff --git a/includes/BacklinkCache.php b/includes/BacklinkCache.php index 02b0f170..8d1571ec 100644 --- a/includes/BacklinkCache.php +++ b/includes/BacklinkCache.php @@ -1,21 +1,71 @@ <?php /** - * Class for fetching backlink lists, approximate backlink counts and partitions. - * Instances of this class should typically be fetched with $title->getBacklinkCache(). + * File for BacklinkCache class + * @file + */ + +/** + * Class for fetching backlink lists, approximate backlink counts and + * partitions. This is a shared cache. + * + * Instances of this class should typically be fetched with the method + * $title->getBacklinkCache(). + * + * Ideally you should only get your backlinks from here when you think + * there is some advantage in caching them. Otherwise it's just a waste + * of memory. * - * Ideally you should only get your backlinks from here when you think there is some - * advantage in caching them. Otherwise it's just a waste of memory. + * Introduced by r47317 + * + * @internal documentation reviewed on 18 Mar 2011 by hashar + * + * @author Tim Starling + * @copyright © 2009, Tim Starling, Domas Mituzas + * @copyright © 2010, Max Sem + * @copyright © 2011, Ashar Voultoiz */ class BacklinkCache { - var $partitionCache = array(); - var $fullResultCache = array(); - var $title; - var $db; + + /** + * Multi dimensions array representing batches. Keys are: + * > (string) links table name + * > 'numRows' : Number of rows for this link table + * > 'batches' : array( $start, $end ) + * + * @see BacklinkCache::partitionResult() + * + * Cleared with BacklinkCache::clear() + */ + protected $partitionCache = array(); + + /** + * Contains the whole links from a database result. + * This is raw data that will be partitioned in $partitionCache + * + * Initialized with BacklinkCache::getLinks() + * Cleared with BacklinkCache::clear() + */ + protected $fullResultCache = array(); + + /** + * Local copy of a database object. + * + * Accessor: BacklinkCache::getDB() + * Mutator : BacklinkCache::setDB() + * Cleared with BacklinkCache::clear() + */ + protected $db; + + /** + * Local copy of a Title object + */ + protected $title; const CACHE_EXPIRY = 3600; /** * Create a new BacklinkCache + * @param Title $title : Title object to create a backlink cache for. */ function __construct( $title ) { $this->title = $title; @@ -23,16 +73,17 @@ class BacklinkCache { /** * Serialization handler, diasallows to serialize the database to prevent - * failures after this class is deserialized from cache with dead DB connection. + * failures after this class is deserialized from cache with dead DB + * connection. */ function __sleep() { return array( 'partitionCache', 'fullResultCache', 'title' ); } /** - * Clear locally stored data + * Clear locally stored data and database object. */ - function clear() { + public function clear() { $this->partitionCache = array(); $this->fullResultCache = array(); unset( $this->db ); @@ -40,11 +91,18 @@ class BacklinkCache { /** * Set the Database object to use + * + * @param $db DatabaseBase */ public function setDB( $db ) { $this->db = $db; } + /** + * Get the slave connection to the database + * When non existing, will initialize the connection. + * @return Database object + */ protected function getDB() { if ( !isset( $this->db ) ) { $this->db = wfGetDB( DB_SLAVE ); @@ -58,7 +116,7 @@ class BacklinkCache { * @param $table String * @param $startId Integer or false * @param $endId Integer or false - * @return TitleArray + * @return TitleArrayFromResult */ public function getLinks( $table, $startId = false, $endId = false ) { wfProfileIn( __METHOD__ ); @@ -95,6 +153,7 @@ class BacklinkCache { return $ta; } + // @todo FIXME: Make this a function? if ( !isset( $this->fullResultCache[$table] ) ) { wfDebug( __METHOD__ . ": from DB\n" ); $res = $this->getDB()->select( @@ -117,14 +176,15 @@ class BacklinkCache { /** * Get the field name prefix for a given table + * @param $table String */ protected function getPrefix( $table ) { static $prefixes = array( - 'pagelinks' => 'pl', - 'imagelinks' => 'il', + 'pagelinks' => 'pl', + 'imagelinks' => 'il', 'categorylinks' => 'cl', 'templatelinks' => 'tl', - 'redirect' => 'rd', + 'redirect' => 'rd', ); if ( isset( $prefixes[$table] ) ) { @@ -135,18 +195,32 @@ class BacklinkCache { } /** - * Get the SQL condition array for selecting backlinks, with a join on the page table + * Get the SQL condition array for selecting backlinks, with a join + * on the page table. + * @param $table String */ protected function getConditions( $table ) { $prefix = $this->getPrefix( $table ); + // @todo FIXME: imagelinks and categorylinks do not rely on getNamespace, + // they could be moved up for nicer case statements switch ( $table ) { case 'pagelinks': case 'templatelinks': + $conds = array( + "{$prefix}_namespace" => $this->title->getNamespace(), + "{$prefix}_title" => $this->title->getDBkey(), + "page_id={$prefix}_from" + ); + break; case 'redirect': $conds = array( "{$prefix}_namespace" => $this->title->getNamespace(), - "{$prefix}_title" => $this->title->getDBkey(), + "{$prefix}_title" => $this->title->getDBkey(), + $this->getDb()->makeList( array( + "{$prefix}_interwiki = ''", + "{$prefix}_interwiki is null", + ), LIST_OR ), "page_id={$prefix}_from" ); break; @@ -171,6 +245,8 @@ class BacklinkCache { /** * Get the approximate number of backlinks + * @param $table String + * @return integer */ public function getNumLinks( $table ) { if ( isset( $this->fullResultCache[$table] ) ) { @@ -189,15 +265,17 @@ class BacklinkCache { /** * Partition the backlinks into batches. - * Returns an array giving the start and end of each range. The first batch has - * a start of false, and the last batch has an end of false. + * Returns an array giving the start and end of each range. The first + * batch has a start of false, and the last batch has an end of false. * * @param $table String: the links table name * @param $batchSize Integer * @return Array */ public function partition( $table, $batchSize ) { - // Try cache + + // 1) try partition cache ... + if ( isset( $this->partitionCache[$table][$batchSize] ) ) { wfDebug( __METHOD__ . ": got from partition cache\n" ); return $this->partitionCache[$table][$batchSize]['batches']; @@ -206,7 +284,8 @@ class BacklinkCache { $this->partitionCache[$table][$batchSize] = false; $cacheEntry =& $this->partitionCache[$table][$batchSize]; - // Try full result cache + // 2) ... then try full result cache ... + if ( isset( $this->fullResultCache[$table] ) ) { $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize ); wfDebug( __METHOD__ . ": got from full result cache\n" ); @@ -214,7 +293,8 @@ class BacklinkCache { return $cacheEntry['batches']; } - // Try memcached + // 3) ... fallback to memcached ... + global $wgMemc; $memcKey = wfMemcKey( @@ -233,7 +313,9 @@ class BacklinkCache { return $cacheEntry['batches']; } - // Fetch from database + + // 4) ... finally fetch from the slow database :( + $this->getLinks( $table ); $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize ); // Save to memcached @@ -245,6 +327,9 @@ class BacklinkCache { /** * Partition a DB result with backlinks in it into batches + * @param $res ResultWrapper database result + * @param $batchSize integer + * @return array @see */ protected function partitionResult( $res, $batchSize ) { $batches = array(); diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php deleted file mode 100644 index 63c96de7..00000000 --- a/includes/BagOStuff.php +++ /dev/null @@ -1,906 +0,0 @@ -<?php -/** - * Classes to cache objects in PHP accelerators, SQL database or DBA files - * - * Copyright © 2003-2004 Brion Vibber <brion@pobox.com> - * http://www.mediawiki.org/ - * - * 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 - * @ingroup Cache - */ - -/** - * @defgroup Cache Cache - */ - -/** - * interface is intended to be more or less compatible with - * the PHP memcached client. - * - * backends for local hash array and SQL table included: - * <code> - * $bag = new HashBagOStuff(); - * $bag = new SqlBagOStuff(); # connect to db first - * </code> - * - * @ingroup Cache - */ -abstract class BagOStuff { - var $debugMode = false; - - public function set_debug( $bool ) { - $this->debugMode = $bool; - } - - /* *** THE GUTS OF THE OPERATION *** */ - /* Override these with functional things in subclasses */ - - /** - * Get an item with the given key. Returns false if it does not exist. - * @param $key string - */ - abstract public function get( $key ); - - /** - * Set an item. - * @param $key string - * @param $value mixed - * @param $exptime int Either an interval in seconds or a unix timestamp for expiry - */ - abstract public function set( $key, $value, $exptime = 0 ); - - /* - * Delete an item. - * @param $key string - * @param $time int Amount of time to delay the operation (mostly memcached-specific) - */ - abstract public function delete( $key, $time = 0 ); - - public function lock( $key, $timeout = 0 ) { - /* stub */ - return true; - } - - public function unlock( $key ) { - /* stub */ - return true; - } - - public function keys() { - /* stub */ - return array(); - } - - /* *** Emulated functions *** */ - /* Better performance can likely be got with custom written versions */ - public function get_multi( $keys ) { - $out = array(); - - foreach ( $keys as $key ) { - $out[$key] = $this->get( $key ); - } - - return $out; - } - - public function set_multi( $hash, $exptime = 0 ) { - foreach ( $hash as $key => $value ) { - $this->set( $key, $value, $exptime ); - } - } - - public function add( $key, $value, $exptime = 0 ) { - if ( !$this->get( $key ) ) { - $this->set( $key, $value, $exptime ); - - return true; - } - } - - public function add_multi( $hash, $exptime = 0 ) { - foreach ( $hash as $key => $value ) { - $this->add( $key, $value, $exptime ); - } - } - - public function delete_multi( $keys, $time = 0 ) { - foreach ( $keys as $key ) { - $this->delete( $key, $time ); - } - } - - public function replace( $key, $value, $exptime = 0 ) { - if ( $this->get( $key ) !== false ) { - $this->set( $key, $value, $exptime ); - } - } - - /** - * @param $key String: Key to increase - * @param $value Integer: Value to add to $key (Default 1) - * @return null if lock is not possible else $key value increased by $value - */ - public function incr( $key, $value = 1 ) { - if ( !$this->lock( $key ) ) { - return null; - } - - $value = intval( $value ); - - if ( ( $n = $this->get( $key ) ) !== false ) { - $n += $value; - $this->set( $key, $n ); // exptime? - } - $this->unlock( $key ); - - return $n; - } - - public function decr( $key, $value = 1 ) { - return $this->incr( $key, - $value ); - } - - public function debug( $text ) { - if ( $this->debugMode ) { - wfDebug( "BagOStuff debug: $text\n" ); - } - } - - /** - * Convert an optionally relative time to an absolute time - */ - protected function convertExpiry( $exptime ) { - if ( ( $exptime != 0 ) && ( $exptime < 86400 * 3650 /* 10 years */ ) ) { - return time() + $exptime; - } else { - return $exptime; - } - } -} - -/** - * Functional versions! - * This is a test of the interface, mainly. It stores things in an associative - * array, which is not going to persist between program runs. - * - * @ingroup Cache - */ -class HashBagOStuff extends BagOStuff { - var $bag; - - function __construct() { - $this->bag = array(); - } - - protected function expire( $key ) { - $et = $this->bag[$key][1]; - - if ( ( $et == 0 ) || ( $et > time() ) ) { - return false; - } - - $this->delete( $key ); - - return true; - } - - function get( $key ) { - if ( !isset( $this->bag[$key] ) ) { - return false; - } - - if ( $this->expire( $key ) ) { - return false; - } - - return $this->bag[$key][0]; - } - - function set( $key, $value, $exptime = 0 ) { - $this->bag[$key] = array( $value, $this->convertExpiry( $exptime ) ); - } - - function delete( $key, $time = 0 ) { - if ( !isset( $this->bag[$key] ) ) { - return false; - } - - unset( $this->bag[$key] ); - - return true; - } - - function keys() { - return array_keys( $this->bag ); - } -} - -/** - * Class to store objects in the database - * - * @ingroup Cache - */ -class SqlBagOStuff extends BagOStuff { - var $lb, $db; - var $lastExpireAll = 0; - - protected function getDB() { - global $wgDBtype; - - if ( !isset( $this->db ) ) { - /* We must keep a separate connection to MySQL in order to avoid deadlocks - * However, SQLite has an opposite behaviour. - * @todo Investigate behaviour for other databases - */ - if ( $wgDBtype == 'sqlite' ) { - $this->db = wfGetDB( DB_MASTER ); - } else { - $this->lb = wfGetLBFactory()->newMainLB(); - $this->db = $this->lb->getConnection( DB_MASTER ); - $this->db->clearFlag( DBO_TRX ); - } - } - - return $this->db; - } - - public function get( $key ) { - # expire old entries if any - $this->garbageCollect(); - $db = $this->getDB(); - $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ), - array( 'keyname' => $key ), __METHOD__ ); - - if ( !$row ) { - $this->debug( 'get: no matching rows' ); - return false; - } - - $this->debug( "get: retrieved data; expiry time is " . $row->exptime ); - - if ( $this->isExpired( $row->exptime ) ) { - $this->debug( "get: key has expired, deleting" ); - try { - $db->begin(); - # Put the expiry time in the WHERE condition to avoid deleting a - # newly-inserted value - $db->delete( 'objectcache', - array( - 'keyname' => $key, - 'exptime' => $row->exptime - ), __METHOD__ ); - $db->commit(); - } catch ( DBQueryError $e ) { - $this->handleWriteError( $e ); - } - - return false; - } - - return $this->unserialize( $db->decodeBlob( $row->value ) ); - } - - public function set( $key, $value, $exptime = 0 ) { - $db = $this->getDB(); - $exptime = intval( $exptime ); - - if ( $exptime < 0 ) { - $exptime = 0; - } - - if ( $exptime == 0 ) { - $encExpiry = $this->getMaxDateTime(); - } else { - if ( $exptime < 3.16e8 ) { # ~10 years - $exptime += time(); - } - - $encExpiry = $db->timestamp( $exptime ); - } - try { - $db->begin(); - // (bug 24425) use a replace if the db supports it instead of - // delete/insert to avoid clashes with conflicting keynames - $db->replace( 'objectcache', array( 'keyname' ), - array( - 'keyname' => $key, - 'value' => $db->encodeBlob( $this->serialize( $value ) ), - 'exptime' => $encExpiry - ), __METHOD__ ); - $db->commit(); - } catch ( DBQueryError $e ) { - $this->handleWriteError( $e ); - - return false; - } - - return true; - } - - public function delete( $key, $time = 0 ) { - $db = $this->getDB(); - - try { - $db->begin(); - $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ ); - $db->commit(); - } catch ( DBQueryError $e ) { - $this->handleWriteError( $e ); - - return false; - } - - return true; - } - - public function incr( $key, $step = 1 ) { - $db = $this->getDB(); - $step = intval( $step ); - - try { - $db->begin(); - $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ), - array( 'keyname' => $key ), __METHOD__, array( 'FOR UPDATE' ) ); - if ( $row === false ) { - // Missing - $db->commit(); - - return null; - } - $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ ); - if ( $this->isExpired( $row->exptime ) ) { - // Expired, do not reinsert - $db->commit(); - - return null; - } - - $oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) ); - $newValue = $oldValue + $step; - $db->insert( 'objectcache', - array( - 'keyname' => $key, - 'value' => $db->encodeBlob( $this->serialize( $newValue ) ), - 'exptime' => $row->exptime - ), __METHOD__, 'IGNORE' ); - - if ( $db->affectedRows() == 0 ) { - // Race condition. See bug 28611 - $newValue = null; - } - $db->commit(); - } catch ( DBQueryError $e ) { - $this->handleWriteError( $e ); - - return null; - } - - return $newValue; - } - - public function keys() { - $db = $this->getDB(); - $res = $db->select( 'objectcache', array( 'keyname' ), false, __METHOD__ ); - $result = array(); - - foreach ( $res as $row ) { - $result[] = $row->keyname; - } - - return $result; - } - - protected function isExpired( $exptime ) { - return $exptime != $this->getMaxDateTime() && wfTimestamp( TS_UNIX, $exptime ) < time(); - } - - protected function getMaxDateTime() { - if ( time() > 0x7fffffff ) { - return $this->getDB()->timestamp( 1 << 62 ); - } else { - return $this->getDB()->timestamp( 0x7fffffff ); - } - } - - protected function garbageCollect() { - /* Ignore 99% of requests */ - if ( !mt_rand( 0, 100 ) ) { - $now = time(); - /* Avoid repeating the delete within a few seconds */ - if ( $now > ( $this->lastExpireAll + 1 ) ) { - $this->lastExpireAll = $now; - $this->expireAll(); - } - } - } - - public function expireAll() { - $db = $this->getDB(); - $now = $db->timestamp(); - - try { - $db->begin(); - $db->delete( 'objectcache', array( 'exptime < ' . $db->addQuotes( $now ) ), __METHOD__ ); - $db->commit(); - } catch ( DBQueryError $e ) { - $this->handleWriteError( $e ); - } - } - - public function deleteAll() { - $db = $this->getDB(); - - try { - $db->begin(); - $db->delete( 'objectcache', '*', __METHOD__ ); - $db->commit(); - } catch ( DBQueryError $e ) { - $this->handleWriteError( $e ); - } - } - - /** - * Serialize an object and, if possible, compress the representation. - * On typical message and page data, this can provide a 3X decrease - * in storage requirements. - * - * @param $data mixed - * @return string - */ - protected function serialize( &$data ) { - $serial = serialize( $data ); - - if ( function_exists( 'gzdeflate' ) ) { - return gzdeflate( $serial ); - } else { - return $serial; - } - } - - /** - * Unserialize and, if necessary, decompress an object. - * @param $serial string - * @return mixed - */ - protected function unserialize( $serial ) { - if ( function_exists( 'gzinflate' ) ) { - $decomp = @gzinflate( $serial ); - - if ( false !== $decomp ) { - $serial = $decomp; - } - } - - $ret = unserialize( $serial ); - - return $ret; - } - - /** - * Handle a DBQueryError which occurred during a write operation. - * Ignore errors which are due to a read-only database, rethrow others. - */ - protected function handleWriteError( $exception ) { - $db = $this->getDB(); - - if ( !$db->wasReadOnlyError() ) { - throw $exception; - } - - try { - $db->rollback(); - } catch ( DBQueryError $e ) { - } - - wfDebug( __METHOD__ . ": ignoring query error\n" ); - $db->ignoreErrors( false ); - } -} - -/** - * Backwards compatibility alias - */ -class MediaWikiBagOStuff extends SqlBagOStuff { } - -/** - * This is a wrapper for APC's shared memory functions - * - * @ingroup Cache - */ -class APCBagOStuff extends BagOStuff { - public function get( $key ) { - $val = apc_fetch( $key ); - - if ( is_string( $val ) ) { - $val = unserialize( $val ); - } - - return $val; - } - - public function set( $key, $value, $exptime = 0 ) { - apc_store( $key, serialize( $value ), $exptime ); - - return true; - } - - public function delete( $key, $time = 0 ) { - apc_delete( $key ); - - return true; - } - - public function keys() { - $info = apc_cache_info( 'user' ); - $list = $info['cache_list']; - $keys = array(); - - foreach ( $list as $entry ) { - $keys[] = $entry['info']; - } - - return $keys; - } -} - -/** - * This is a wrapper for eAccelerator's shared memory functions. - * - * This is basically identical to the deceased Turck MMCache version, - * mostly because eAccelerator is based on Turck MMCache. - * - * @ingroup Cache - */ -class eAccelBagOStuff extends BagOStuff { - public function get( $key ) { - $val = eaccelerator_get( $key ); - - if ( is_string( $val ) ) { - $val = unserialize( $val ); - } - - return $val; - } - - public function set( $key, $value, $exptime = 0 ) { - eaccelerator_put( $key, serialize( $value ), $exptime ); - - return true; - } - - public function delete( $key, $time = 0 ) { - eaccelerator_rm( $key ); - - return true; - } - - public function lock( $key, $waitTimeout = 0 ) { - eaccelerator_lock( $key ); - - return true; - } - - public function unlock( $key ) { - eaccelerator_unlock( $key ); - - return true; - } -} - -/** - * Wrapper for XCache object caching functions; identical interface - * to the APC wrapper - * - * @ingroup Cache - */ -class XCacheBagOStuff extends BagOStuff { - /** - * Get a value from the XCache object cache - * - * @param $key String: cache key - * @return mixed - */ - public function get( $key ) { - $val = xcache_get( $key ); - - if ( is_string( $val ) ) { - $val = unserialize( $val ); - } - - return $val; - } - - /** - * Store a value in the XCache object cache - * - * @param $key String: cache key - * @param $value Mixed: object to store - * @param $expire Int: expiration time - * @return bool - */ - public function set( $key, $value, $expire = 0 ) { - xcache_set( $key, serialize( $value ), $expire ); - - return true; - } - - /** - * Remove a value from the XCache object cache - * - * @param $key String: cache key - * @param $time Int: not used in this implementation - * @return bool - */ - public function delete( $key, $time = 0 ) { - xcache_unset( $key ); - - return true; - } -} - -/** - * Cache that uses DBA as a backend. - * Slow due to the need to constantly open and close the file to avoid holding - * writer locks. Intended for development use only, as a memcached workalike - * for systems that don't have it. - * - * @ingroup Cache - */ -class DBABagOStuff extends BagOStuff { - var $mHandler, $mFile, $mReader, $mWriter, $mDisabled; - - public function __construct( $dir = false ) { - global $wgDBAhandler; - - if ( $dir === false ) { - global $wgTmpDirectory; - $dir = $wgTmpDirectory; - } - - $this->mFile = "$dir/mw-cache-" . wfWikiID(); - $this->mFile .= '.db'; - wfDebug( __CLASS__ . ": using cache file {$this->mFile}\n" ); - $this->mHandler = $wgDBAhandler; - } - - /** - * Encode value and expiry for storage - */ - function encode( $value, $expiry ) { - # Convert to absolute time - $expiry = $this->convertExpiry( $expiry ); - - return sprintf( '%010u', intval( $expiry ) ) . ' ' . serialize( $value ); - } - - /** - * @return list containing value first and expiry second - */ - function decode( $blob ) { - if ( !is_string( $blob ) ) { - return array( null, 0 ); - } else { - return array( - unserialize( substr( $blob, 11 ) ), - intval( substr( $blob, 0, 10 ) ) - ); - } - } - - function getReader() { - if ( file_exists( $this->mFile ) ) { - $handle = dba_open( $this->mFile, 'rl', $this->mHandler ); - } else { - $handle = $this->getWriter(); - } - - if ( !$handle ) { - wfDebug( "Unable to open DBA cache file {$this->mFile}\n" ); - } - - return $handle; - } - - function getWriter() { - $handle = dba_open( $this->mFile, 'cl', $this->mHandler ); - - if ( !$handle ) { - wfDebug( "Unable to open DBA cache file {$this->mFile}\n" ); - } - - return $handle; - } - - function get( $key ) { - wfProfileIn( __METHOD__ ); - wfDebug( __METHOD__ . "($key)\n" ); - - $handle = $this->getReader(); - if ( !$handle ) { - wfProfileOut( __METHOD__ ); - return null; - } - - $val = dba_fetch( $key, $handle ); - list( $val, $expiry ) = $this->decode( $val ); - - # Must close ASAP because locks are held - dba_close( $handle ); - - if ( !is_null( $val ) && $expiry && $expiry < time() ) { - # Key is expired, delete it - $handle = $this->getWriter(); - dba_delete( $key, $handle ); - dba_close( $handle ); - wfDebug( __METHOD__ . ": $key expired\n" ); - $val = null; - } - - wfProfileOut( __METHOD__ ); - return $val; - } - - function set( $key, $value, $exptime = 0 ) { - wfProfileIn( __METHOD__ ); - wfDebug( __METHOD__ . "($key)\n" ); - - $blob = $this->encode( $value, $exptime ); - - $handle = $this->getWriter(); - if ( !$handle ) { - wfProfileOut( __METHOD__ ); - return false; - } - - $ret = dba_replace( $key, $blob, $handle ); - dba_close( $handle ); - - wfProfileOut( __METHOD__ ); - return $ret; - } - - function delete( $key, $time = 0 ) { - wfProfileIn( __METHOD__ ); - wfDebug( __METHOD__ . "($key)\n" ); - - $handle = $this->getWriter(); - if ( !$handle ) { - wfProfileOut( __METHOD__ ); - return false; - } - - $ret = dba_delete( $key, $handle ); - dba_close( $handle ); - - wfProfileOut( __METHOD__ ); - return $ret; - } - - function add( $key, $value, $exptime = 0 ) { - wfProfileIn( __METHOD__ ); - - $blob = $this->encode( $value, $exptime ); - - $handle = $this->getWriter(); - - if ( !$handle ) { - wfProfileOut( __METHOD__ ); - return false; - } - - $ret = dba_insert( $key, $blob, $handle ); - - # Insert failed, check to see if it failed due to an expired key - if ( !$ret ) { - list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) ); - - if ( $expiry < time() ) { - # Yes expired, delete and try again - dba_delete( $key, $handle ); - $ret = dba_insert( $key, $blob, $handle ); - # This time if it failed then it will be handled by the caller like any other race - } - } - - dba_close( $handle ); - - wfProfileOut( __METHOD__ ); - return $ret; - } - - function keys() { - $reader = $this->getReader(); - $k1 = dba_firstkey( $reader ); - - if ( !$k1 ) { - return array(); - } - - $result[] = $k1; - - while ( $key = dba_nextkey( $reader ) ) { - $result[] = $key; - } - - return $result; - } -} - -/** - * Wrapper for WinCache object caching functions; identical interface - * to the APC wrapper - * - * @ingroup Cache - */ -class WinCacheBagOStuff extends BagOStuff { - - /** - * Get a value from the WinCache object cache - * - * @param $key String: cache key - * @return mixed - */ - public function get( $key ) { - $val = wincache_ucache_get( $key ); - - if ( is_string( $val ) ) { - $val = unserialize( $val ); - } - - return $val; - } - - /** - * Store a value in the WinCache object cache - * - * @param $key String: cache key - * @param $value Mixed: object to store - * @param $expire Int: expiration time - * @return bool - */ - public function set( $key, $value, $expire = 0 ) { - wincache_ucache_set( $key, serialize( $value ), $expire ); - - return true; - } - - /** - * Remove a value from the WinCache object cache - * - * @param $key String: cache key - * @param $time Int: not used in this implementation - * @return bool - */ - public function delete( $key, $time = 0 ) { - wincache_ucache_delete( $key ); - - return true; - } - - public function keys() { - $info = wincache_ucache_info(); - $list = $info['ucache_entries']; - $keys = array(); - - foreach ( $list as $entry ) { - $keys[] = $entry['key_name']; - } - - return $keys; - } -} diff --git a/includes/Block.php b/includes/Block.php index 7c5f0ddd..27181d86 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -1,53 +1,93 @@ <?php /** - * @file * Blocks and bans object - */ - -/** - * The block class - * All the functions in this class assume the object is either explicitly - * loaded or filled. It is not load-on-demand. There are no accessors. * - * Globals used: $wgAutoblockExpiry, $wgAntiLockFlags + * 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. * - * @todo This could be used everywhere, but it isn't. + * 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 Block { - /* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry, - $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName, - $mBlockEmail, $mByName, $mAngryAutoblock, $mAllowUsertalk; - /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster; + /* public*/ var $mReason, $mTimestamp, $mAuto, $mExpiry, $mHideName; + + protected + $mId, + $mFromMaster, - const EB_KEEP_EXPIRED = 1; - const EB_FOR_UPDATE = 2; - const EB_RANGE_ONLY = 4; + $mBlockEmail, + $mDisableUsertalk, + $mCreateAccount; + /// @var User|String + protected $target; + + /// @var Block::TYPE_ constant. Can only be USER, IP or RANGE internally + protected $type; + + /// @var User + protected $blocker; + + /// @var Bool + protected $isHardblock = true; + + /// @var Bool + protected $isAutoblocking = true; + + # TYPE constants + const TYPE_USER = 1; + const TYPE_IP = 2; + const TYPE_RANGE = 3; + const TYPE_AUTO = 4; + const TYPE_ID = 5; + + /** + * Constructor + * @todo FIXME: Don't know what the best format to have for this constructor is, but fourteen + * optional parameters certainly isn't it. + */ function __construct( $address = '', $user = 0, $by = 0, $reason = '', $timestamp = 0, $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, - $hideName = 0, $blockEmail = 0, $allowUsertalk = 0, $byName = false ) + $hideName = 0, $blockEmail = 0, $allowUsertalk = 0 ) { - $this->mId = 0; - # Expand valid IPv6 addresses - $address = IP::sanitizeIP( $address ); - $this->mAddress = $address; - $this->mUser = $user; - $this->mBy = $by; + if( $timestamp === 0 ){ + $timestamp = wfTimestampNow(); + } + + if( count( func_get_args() ) > 0 ){ + # Soon... :D + # wfDeprecated( __METHOD__ . " with arguments" ); + } + + $this->setTarget( $address ); + $this->setBlocker( User::newFromID( $by ) ); $this->mReason = $reason; $this->mTimestamp = wfTimestamp( TS_MW, $timestamp ); $this->mAuto = $auto; - $this->mAnonOnly = $anonOnly; - $this->mCreateAccount = $createAccount; - $this->mExpiry = self::decodeExpiry( $expiry ); - $this->mEnableAutoblock = $enableAutoblock; + $this->isHardblock( !$anonOnly ); + $this->prevents( 'createaccount', $createAccount ); + if ( $expiry == 'infinity' || $expiry == Block::infinity() ) { + $this->mExpiry = 'infinity'; + } else { + $this->mExpiry = wfTimestamp( TS_MW, $expiry ); + } + $this->isAutoblocking( $enableAutoblock ); $this->mHideName = $hideName; - $this->mBlockEmail = $blockEmail; - $this->mAllowUsertalk = $allowUsertalk; - $this->mForUpdate = false; + $this->prevents( 'sendemail', $blockEmail ); + $this->prevents( 'editownusertalk', !$allowUsertalk ); + $this->mFromMaster = false; - $this->mByName = $byName; - $this->mAngryAutoblock = false; - $this->initialiseRange(); } /** @@ -57,56 +97,54 @@ class Block { * * @param $address String: IP address of user/anon * @param $user Integer: user id of user - * @param $killExpired Boolean: delete expired blocks on load * @return Block Object + * @deprecated since 1.18 */ - public static function newFromDB( $address, $user = 0, $killExpired = true ) { - $block = new Block; - $block->load( $address, $user, $killExpired ); - - if ( $block->isValid() ) { - return $block; - } else { - return null; - } + public static function newFromDB( $address, $user = 0 ) { + return self::newFromTarget( User::whoIs( $user ), $address ); } /** * Load a blocked user from their block id. * * @param $id Integer: Block id to search for - * @return Block object + * @return Block object or null */ public static function newFromID( $id ) { $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*', - array( 'ipb_id' => $id ), __METHOD__ ) ); - $block = new Block; - - if ( $block->loadFromResult( $res ) ) { - return $block; + $res = $dbr->selectRow( + 'ipblocks', + '*', + array( 'ipb_id' => $id ), + __METHOD__ + ); + if ( $res ) { + return Block::newFromRow( $res ); } else { return null; } } /** - * Check if two blocks are effectively equal + * Check if two blocks are effectively equal. Doesn't check irrelevant things like + * the blocking user or the block timestamp, only things which affect the blocked user * * - * @return Boolean + * @param $block Block + * + * @return bool */ public function equals( Block $block ) { return ( - $this->mAddress == $block->mAddress - && $this->mUser == $block->mUser + (string)$this->target == (string)$block->target + && $this->type == $block->type && $this->mAuto == $block->mAuto - && $this->mAnonOnly == $block->mAnonOnly - && $this->mCreateAccount == $block->mCreateAccount + && $this->isHardblock() == $block->isHardblock() + && $this->prevents( 'createaccount' ) == $block->prevents( 'createaccount' ) && $this->mExpiry == $block->mExpiry - && $this->mEnableAutoblock == $block->mEnableAutoblock + && $this->isAutoblocking() == $block->isAutoblocking() && $this->mHideName == $block->mHideName - && $this->mBlockEmail == $block->mBlockEmail - && $this->mAllowUsertalk == $block->mAllowUsertalk + && $this->prevents( 'sendemail' ) == $block->prevents( 'sendemail' ) + && $this->prevents( 'editownusertalk' ) == $block->prevents( 'editownusertalk' ) && $this->mReason == $block->mReason ); } @@ -114,251 +152,232 @@ class Block { /** * Clear all member variables in the current object. Does not clear * the block from the DB. + * @deprecated since 1.18 */ public function clear() { - $this->mAddress = $this->mReason = $this->mTimestamp = ''; - $this->mId = $this->mAnonOnly = $this->mCreateAccount = - $this->mEnableAutoblock = $this->mAuto = $this->mUser = - $this->mBy = $this->mHideName = $this->mBlockEmail = $this->mAllowUsertalk = 0; - $this->mByName = false; + # Noop } /** - * Get the DB object and set the reference parameter to the select options. - * The options array will contain FOR UPDATE if appropriate. + * Get a block from the DB, with either the given address or the given username * - * @param $options Array - * @return Database + * @param $address string The IP address of the user, or blank to skip IP blocks + * @param $user int The user ID, or zero for anonymous users + * @return Boolean: the user is blocked from editing + * @deprecated since 1.18 */ - protected function &getDBOptions( &$options ) { - global $wgAntiLockFlags; + public function load( $address = '', $user = 0 ) { + wfDeprecated( __METHOD__ ); + if( $user ){ + $username = User::whoIs( $user ); + $block = self::newFromTarget( $username, $address ); + } else { + $block = self::newFromTarget( null, $address ); + } - if ( $this->mForUpdate || $this->mFromMaster ) { - $db = wfGetDB( DB_MASTER ); - if ( !$this->mForUpdate || ( $wgAntiLockFlags & ALF_NO_BLOCK_LOCK ) ) { - $options = array(); - } else { - $options = array( 'FOR UPDATE' ); + if( $block instanceof Block ){ + # This is mildly evil, but hey, it's B/C :D + foreach( $block as $variable => $value ){ + $this->$variable = $value; } + return true; } else { - $db = wfGetDB( DB_SLAVE ); - $options = array(); + return false; } - - return $db; } /** - * Get a block from the DB, with either the given address or the given username - * - * @param $address string The IP address of the user, or blank to skip IP blocks - * @param $user int The user ID, or zero for anonymous users - * @param $killExpired bool Whether to delete expired rows while loading - * @return Boolean: the user is blocked from editing - * + * Load a block from the database which affects the already-set $this->target: + * 1) A block directly on the given user or IP + * 2) A rangeblock encompasing the given IP (smallest first) + * 3) An autoblock on the given IP + * @param $vagueTarget User|String also search for blocks affecting this target. Doesn't + * make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups. + * @return Bool whether a relevant block was found */ - public function load( $address = '', $user = 0, $killExpired = true ) { - wfDebug( "Block::load: '$address', '$user', $killExpired\n" ); + protected function newLoad( $vagueTarget = null ) { + $db = wfGetDB( $this->mFromMaster ? DB_MASTER : DB_SLAVE ); - $options = array(); - $db = $this->getDBOptions( $options ); - - if ( 0 == $user && $address === '' ) { - # Invalid user specification, not blocked - $this->clear(); - - return false; + if( $this->type !== null ){ + $conds = array( + 'ipb_address' => array( (string)$this->target ), + ); + } else { + $conds = array( 'ipb_address' => array() ); } - # Try user block - if ( $user ) { - $res = $db->resultObject( $db->select( 'ipblocks', '*', array( 'ipb_user' => $user ), - __METHOD__, $options ) ); - - if ( $this->loadFromResult( $res, $killExpired ) ) { - return true; + # Be aware that the != '' check is explicit, since empty values will be + # passed by some callers (bug 29116) + if( $vagueTarget != ''){ + list( $target, $type ) = self::parseTarget( $vagueTarget ); + switch( $type ) { + case self::TYPE_USER: + # Slightly wierd, but who are we to argue? + $conds['ipb_address'][] = (string)$target; + break; + + case self::TYPE_IP: + $conds['ipb_address'][] = (string)$target; + $conds[] = self::getRangeCond( IP::toHex( $target ) ); + $conds = $db->makeList( $conds, LIST_OR ); + break; + + case self::TYPE_RANGE: + list( $start, $end ) = IP::parseRange( $target ); + $conds['ipb_address'][] = (string)$target; + $conds[] = self::getRangeCond( $start, $end ); + $conds = $db->makeList( $conds, LIST_OR ); + break; + + default: + throw new MWException( "Tried to load block with invalid type" ); } } - # Try IP block - # TODO: improve performance by merging this query with the autoblock one - # Slightly tricky while handling killExpired as well - if ( $address !== '' ) { - $conds = array( 'ipb_address' => $address, 'ipb_auto' => 0 ); - $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); - - if ( $this->loadFromResult( $res, $killExpired ) ) { - if ( $user && $this->mAnonOnly ) { - # Block is marked anon-only - # Whitelist this IP address against autoblocks and range blocks - # (but not account creation blocks -- bug 13611) - if ( !$this->mCreateAccount ) { - $this->clear(); - } - - return false; - } else { - return true; - } - } - } + $res = $db->select( 'ipblocks', '*', $conds, __METHOD__ ); - # Try range block - if ( $this->loadRange( $address, $killExpired, $user ) ) { - if ( $user && $this->mAnonOnly ) { - # Respect account creation blocks on logged-in users -- bug 13611 - if ( !$this->mCreateAccount ) { - $this->clear(); - } + # This result could contain a block on the user, a block on the IP, and a russian-doll + # set of rangeblocks. We want to choose the most specific one, so keep a leader board. + $bestRow = null; - return false; - } else { - return true; - } - } + # Lower will be better + $bestBlockScore = 100; - # Try autoblock - if ( $address ) { - $conds = array( 'ipb_address' => $address, 'ipb_auto' => 1 ); + # This is begging for $this = $bestBlock, but that's not allowed in PHP :( + $bestBlockPreventsEdit = null; - if ( $user ) { - $conds['ipb_anon_only'] = 0; - } + foreach( $res as $row ){ + $block = Block::newFromRow( $row ); - $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + # Don't use expired blocks + if( $block->deleteIfExpired() ){ + continue; + } - if ( $this->loadFromResult( $res, $killExpired ) ) { - return true; + # Don't use anon only blocks on users + if( $this->type == self::TYPE_USER && !$block->isHardblock() ){ + continue; } - } - # Give up - $this->clear(); - return false; - } + if( $block->getType() == self::TYPE_RANGE ){ + # This is the number of bits that are allowed to vary in the block, give + # or take some floating point errors + $end = wfBaseconvert( $block->getRangeEnd(), 16, 10 ); + $start = wfBaseconvert( $block->getRangeStart(), 16, 10 ); + $size = log( $end - $start + 1, 2 ); + + # This has the nice property that a /32 block is ranked equally with a + # single-IP block, which is exactly what it is... + $score = self::TYPE_RANGE - 1 + ( $size / 128 ); - /** - * Fill in member variables from a result wrapper - * - * @param $res ResultWrapper: row from the ipblocks table - * @param $killExpired Boolean: whether to delete expired rows while loading - * @return Boolean - */ - protected function loadFromResult( ResultWrapper $res, $killExpired = true ) { - $ret = false; - - if ( 0 != $res->numRows() ) { - # Get first block - $row = $res->fetchObject(); - $this->initFromRow( $row ); - - if ( $killExpired ) { - # If requested, delete expired rows - do { - $killed = $this->deleteIfExpired(); - if ( $killed ) { - $row = $res->fetchObject(); - if ( $row ) { - $this->initFromRow( $row ); - } - } - } while ( $killed && $row ); - - # If there were any left after the killing finished, return true - if ( $row ) { - $ret = true; - } } else { - $ret = true; + $score = $block->getType(); + } + + if( $score < $bestBlockScore ){ + $bestBlockScore = $score; + $bestRow = $row; + $bestBlockPreventsEdit = $block->prevents( 'edit' ); } } - $res->free(); - return $ret; + if( $bestRow !== null ){ + $this->initFromRow( $bestRow ); + $this->prevents( 'edit', $bestBlockPreventsEdit ); + return true; + } else { + return false; + } } /** - * Search the database for any range blocks matching the given address, and - * load the row if one is found. - * - * @param $address String: IP address range - * @param $killExpired Boolean: whether to delete expired rows while loading - * @param $user Integer: if not 0, then sets ipb_anon_only - * @return Boolean + * Get a set of SQL conditions which will select rangeblocks encompasing a given range + * @param $start String Hexadecimal IP representation + * @param $end String Hexadecimal IP represenation, or null to use $start = $end + * @return String */ - public function loadRange( $address, $killExpired = true, $user = 0 ) { - $iaddr = IP::toHex( $address ); - - if ( $iaddr === false ) { - # Invalid address - return false; + public static function getRangeCond( $start, $end = null ) { + if ( $end === null ) { + $end = $start; } + # Per bug 14634, we want to include relevant active rangeblocks; for + # rangeblocks, we want to include larger ranges which enclose the given + # range. We know that all blocks must be smaller than $wgBlockCIDRLimit, + # so we can improve performance by filtering on a LIKE clause + $chunk = self::getIpFragment( $start ); + $dbr = wfGetDB( DB_SLAVE ); + $like = $dbr->buildLike( $chunk, $dbr->anyString() ); - # Only scan ranges which start in this /16, this improves search speed - # Blocks should not cross a /16 boundary. - $range = substr( $iaddr, 0, 4 ); + # Fairly hard to make a malicious SQL statement out of hex characters, + # but stranger things have happened... + $safeStart = $dbr->addQuotes( $start ); + $safeEnd = $dbr->addQuotes( $end ); - $options = array(); - $db = $this->getDBOptions( $options ); - $conds = array( - 'ipb_range_start' . $db->buildLike( $range, $db->anyString() ), - "ipb_range_start <= '$iaddr'", - "ipb_range_end >= '$iaddr'" + return $dbr->makeList( + array( + "ipb_range_start $like", + "ipb_range_start <= $safeStart", + "ipb_range_end >= $safeEnd", + ), + LIST_AND ); + } - if ( $user ) { - $conds['ipb_anon_only'] = 0; + /** + * Get the component of an IP address which is certain to be the same between an IP + * address and a rangeblock containing that IP address. + * @param $hex String Hexadecimal IP representation + * @return String + */ + protected static function getIpFragment( $hex ) { + global $wgBlockCIDRLimit; + if ( substr( $hex, 0, 3 ) == 'v6-' ) { + return 'v6-' . substr( substr( $hex, 3 ), 0, floor( $wgBlockCIDRLimit['IPv6'] / 4 ) ); + } else { + return substr( $hex, 0, floor( $wgBlockCIDRLimit['IPv4'] / 4 ) ); } - - $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); - $success = $this->loadFromResult( $res, $killExpired ); - - return $success; } /** * Given a database row from the ipblocks table, initialize * member variables - * * @param $row ResultWrapper: a row from the ipblocks table */ - public function initFromRow( $row ) { - $this->mAddress = $row->ipb_address; + protected function initFromRow( $row ) { + $this->setTarget( $row->ipb_address ); + $this->setBlocker( User::newFromId( $row->ipb_by ) ); + $this->mReason = $row->ipb_reason; $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp ); - $this->mUser = $row->ipb_user; - $this->mBy = $row->ipb_by; $this->mAuto = $row->ipb_auto; - $this->mAnonOnly = $row->ipb_anon_only; - $this->mCreateAccount = $row->ipb_create_account; - $this->mEnableAutoblock = $row->ipb_enable_autoblock; - $this->mBlockEmail = $row->ipb_block_email; - $this->mAllowUsertalk = $row->ipb_allow_usertalk; $this->mHideName = $row->ipb_deleted; $this->mId = $row->ipb_id; - $this->mExpiry = self::decodeExpiry( $row->ipb_expiry ); - if ( isset( $row->user_name ) ) { - $this->mByName = $row->user_name; + // I wish I didn't have to do this + $db = wfGetDB( DB_SLAVE ); + if ( $row->ipb_expiry == $db->getInfinity() ) { + $this->mExpiry = 'infinity'; } else { - $this->mByName = $row->ipb_by_text; + $this->mExpiry = wfTimestamp( TS_MW, $row->ipb_expiry ); } - $this->mRangeStart = $row->ipb_range_start; - $this->mRangeEnd = $row->ipb_range_end; + $this->isHardblock( !$row->ipb_anon_only ); + $this->isAutoblocking( $row->ipb_enable_autoblock ); + + $this->prevents( 'createaccount', $row->ipb_create_account ); + $this->prevents( 'sendemail', $row->ipb_block_email ); + $this->prevents( 'editownusertalk', !$row->ipb_allow_usertalk ); } /** - * Once $mAddress has been set, get the range they came from. - * Wrapper for IP::parseRange + * Create a new Block object from a database row + * @param $row ResultWrapper row from the ipblocks table + * @return Block */ - protected function initialiseRange() { - $this->mRangeStart = ''; - $this->mRangeEnd = ''; - - if ( $this->mUser == 0 ) { - list( $this->mRangeStart, $this->mRangeEnd ) = IP::parseRange( $this->mAddress ); - } + public static function newFromRow( $row ){ + $block = new Block; + $block->initFromRow( $row ); + return $block; } /** @@ -371,12 +390,12 @@ class Block { return false; } - if ( !$this->mId ) { - throw new MWException( "Block::delete() now requires that the mId member be filled\n" ); + if ( !$this->getId() ) { + throw new MWException( "Block::delete() requires that the mId member be filled\n" ); } $dbw = wfGetDB( DB_MASTER ); - $dbw->delete( 'ipblocks', array( 'ipb_id' => $this->mId ), __METHOD__ ); + $dbw->delete( 'ipblocks', array( 'ipb_id' => $this->getId() ), __METHOD__ ); return $dbw->affectedRows() > 0; } @@ -385,16 +404,16 @@ class Block { * Insert a block into the block table. Will fail if there is a conflicting * block (same name and options) already in the database. * - * @return Boolean: whether or not the insertion was successful. + * @param $dbw DatabaseBase if you have one available + * @return mixed: false on failure, assoc array on success: + * ('id' => block ID, 'autoIds' => array of autoblock IDs) */ public function insert( $dbw = null ) { wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); - if ( $dbw === null ) + if ( $dbw === null ) { $dbw = wfGetDB( DB_MASTER ); - - $this->validateBlockParams(); - $this->initialiseRange(); + } # Don't collide with expired blocks Block::purgeExpired(); @@ -402,139 +421,125 @@ class Block { $ipb_id = $dbw->nextSequenceValue( 'ipblocks_ipb_id_seq' ); $dbw->insert( 'ipblocks', - array( - 'ipb_id' => $ipb_id, - 'ipb_address' => $this->mAddress, - 'ipb_user' => $this->mUser, - 'ipb_by' => $this->mBy, - 'ipb_by_text' => $this->mByName, - 'ipb_reason' => $this->mReason, - 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ), - 'ipb_auto' => $this->mAuto, - 'ipb_anon_only' => $this->mAnonOnly, - 'ipb_create_account' => $this->mCreateAccount, - 'ipb_enable_autoblock' => $this->mEnableAutoblock, - 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ), - 'ipb_range_start' => $this->mRangeStart, - 'ipb_range_end' => $this->mRangeEnd, - 'ipb_deleted' => intval( $this->mHideName ), // typecast required for SQLite - 'ipb_block_email' => $this->mBlockEmail, - 'ipb_allow_usertalk' => $this->mAllowUsertalk - ), - 'Block::insert', + $this->getDatabaseArray(), + __METHOD__, array( 'IGNORE' ) ); $affected = $dbw->affectedRows(); + $this->mId = $dbw->insertId(); - if ( $affected ) - $this->doRetroactiveAutoblock(); + if ( $affected ) { + $auto_ipd_ids = $this->doRetroactiveAutoblock(); + return array( 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ); + } - return (bool)$affected; + return false; } /** * Update a block in the DB with new parameters. * The ID field needs to be loaded first. + * + * @return Int number of affected rows, which should probably be 1 or something's + * gone slightly awry */ public function update() { wfDebug( "Block::update; timestamp {$this->mTimestamp}\n" ); $dbw = wfGetDB( DB_MASTER ); - $this->validateBlockParams(); - $dbw->update( 'ipblocks', - array( - 'ipb_user' => $this->mUser, - 'ipb_by' => $this->mBy, - 'ipb_by_text' => $this->mByName, - 'ipb_reason' => $this->mReason, - 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ), - 'ipb_auto' => $this->mAuto, - 'ipb_anon_only' => $this->mAnonOnly, - 'ipb_create_account' => $this->mCreateAccount, - 'ipb_enable_autoblock' => $this->mEnableAutoblock, - 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ), - 'ipb_range_start' => $this->mRangeStart, - 'ipb_range_end' => $this->mRangeEnd, - 'ipb_deleted' => $this->mHideName, - 'ipb_block_email' => $this->mBlockEmail, - 'ipb_allow_usertalk' => $this->mAllowUsertalk - ), - array( 'ipb_id' => $this->mId ), - 'Block::update' + $this->getDatabaseArray( $dbw ), + array( 'ipb_id' => $this->getId() ), + __METHOD__ ); return $dbw->affectedRows(); } /** - * Make sure all the proper members are set to sane values - * before adding/updating a block + * Get an array suitable for passing to $dbw->insert() or $dbw->update() + * @param $db DatabaseBase + * @return Array */ - protected function validateBlockParams() { - # Unset ipb_anon_only for user blocks, makes no sense - if ( $this->mUser ) { - $this->mAnonOnly = 0; + protected function getDatabaseArray( $db = null ){ + if( !$db ){ + $db = wfGetDB( DB_SLAVE ); } + $expiry = $db->encodeExpiry( $this->mExpiry ); + + $a = array( + 'ipb_address' => (string)$this->target, + 'ipb_user' => $this->target instanceof User ? $this->target->getID() : 0, + 'ipb_by' => $this->getBlocker()->getId(), + 'ipb_by_text' => $this->getBlocker()->getName(), + 'ipb_reason' => $this->mReason, + 'ipb_timestamp' => $db->timestamp( $this->mTimestamp ), + 'ipb_auto' => $this->mAuto, + 'ipb_anon_only' => !$this->isHardblock(), + 'ipb_create_account' => $this->prevents( 'createaccount' ), + 'ipb_enable_autoblock' => $this->isAutoblocking(), + 'ipb_expiry' => $expiry, + 'ipb_range_start' => $this->getRangeStart(), + 'ipb_range_end' => $this->getRangeEnd(), + 'ipb_deleted' => intval( $this->mHideName ), // typecast required for SQLite + 'ipb_block_email' => $this->prevents( 'sendemail' ), + 'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ) + ); - # Unset ipb_enable_autoblock for IP blocks, makes no sense - if ( !$this->mUser ) { - $this->mEnableAutoblock = 0; - } + return $a; + } - # bug 18860: non-anon-only IP blocks should be allowed to block email - if ( !$this->mUser && $this->mAnonOnly ) { - $this->mBlockEmail = 0; - } + /** + * Retroactively autoblocks the last IP used by the user (if it is a user) + * blocked by this Block. + * + * @return Array: block IDs of retroactive autoblocks made + */ + protected function doRetroactiveAutoblock() { + $blockIds = array(); + # If autoblock is enabled, autoblock the LAST IP(s) used + if ( $this->isAutoblocking() && $this->getType() == self::TYPE_USER ) { + wfDebug( "Doing retroactive autoblocks for " . $this->getTarget() . "\n" ); - if ( !$this->mByName ) { - if ( $this->mBy ) { - $this->mByName = User::whoIs( $this->mBy ); - } else { - global $wgUser; - $this->mByName = $wgUser->getName(); + $continue = wfRunHooks( + 'PerformRetroactiveAutoblock', array( $this, &$blockIds ) ); + + if ( $continue ) { + self::defaultRetroactiveAutoblock( $this, $blockIds ); } } + return $blockIds; } /** * Retroactively autoblocks the last IP used by the user (if it is a user) - * blocked by this Block. + * blocked by this Block. This will use the recentchanges table. * - * @return Boolean: whether or not a retroactive autoblock was made. + * @param Block $block + * @param Array &$blockIds + * @return Array: block IDs of retroactive autoblocks made */ - public function doRetroactiveAutoblock() { + protected static function defaultRetroactiveAutoblock( Block $block, array &$blockIds ) { $dbr = wfGetDB( DB_SLAVE ); - # If autoblock is enabled, autoblock the LAST IP used - # - stolen shamelessly from CheckUser_body.php - if ( $this->mEnableAutoblock && $this->mUser ) { - wfDebug( "Doing retroactive autoblocks for " . $this->mAddress . "\n" ); + $options = array( 'ORDER BY' => 'rc_timestamp DESC' ); + $conds = array( 'rc_user_text' => (string)$block->getTarget() ); - $options = array( 'ORDER BY' => 'rc_timestamp DESC' ); - $conds = array( 'rc_user_text' => $this->mAddress ); - - if ( $this->mAngryAutoblock ) { - // Block any IP used in the last 7 days. Up to five IPs. - $conds[] = 'rc_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( time() - ( 7 * 86400 ) ) ); - $options['LIMIT'] = 5; - } else { - // Just the last IP used. - $options['LIMIT'] = 1; - } + // Just the last IP used. + $options['LIMIT'] = 1; - $res = $dbr->select( 'recentchanges', array( 'rc_ip' ), $conds, - __METHOD__ , $options ); + $res = $dbr->select( 'recentchanges', array( 'rc_ip' ), $conds, + __METHOD__ , $options ); - if ( !$dbr->numRows( $res ) ) { - # No results, don't autoblock anything - wfDebug( "No IP found to retroactively autoblock\n" ); - } else { - foreach ( $res as $row ) { - if ( $row->rc_ip ) { - $this->doAutoblock( $row->rc_ip ); - } + if ( !$dbr->numRows( $res ) ) { + # No results, don't autoblock anything + wfDebug( "No IP found to retroactively autoblock\n" ); + } else { + foreach ( $res as $row ) { + if ( $row->rc_ip ) { + $id = $block->doAutoblock( $row->rc_ip ); + if ( $id ) $blockIds[] = $id; } } } @@ -542,6 +547,7 @@ class Block { /** * Checks whether a given IP is on the autoblock whitelist. + * TODO: this probably belongs somewhere else, but not sure where... * * @param $ip String: The IP to check * @return Boolean @@ -587,73 +593,70 @@ class Block { * Autoblocks the given IP, referring to this Block. * * @param $autoblockIP String: the IP to autoblock. - * @param $justInserted Boolean: the main block was just inserted - * @return Boolean: whether or not an autoblock was inserted. + * @return mixed: block ID if an autoblock was inserted, false if not. */ - public function doAutoblock( $autoblockIP, $justInserted = false ) { + public function doAutoblock( $autoblockIP ) { # If autoblocks are disabled, go away. - if ( !$this->mEnableAutoblock ) { - return; + if ( !$this->isAutoblocking() ) { + return false; } - # Check for presence on the autoblock whitelist - if ( Block::isWhitelistedFromAutoblocks( $autoblockIP ) ) { - return; + # Check for presence on the autoblock whitelist. + if ( self::isWhitelistedFromAutoblocks( $autoblockIP ) ) { + return false; } - # # Allow hooks to cancel the autoblock. + # Allow hooks to cancel the autoblock. if ( !wfRunHooks( 'AbortAutoblock', array( $autoblockIP, &$this ) ) ) { wfDebug( "Autoblock aborted by hook.\n" ); return false; } - # It's okay to autoblock. Go ahead and create/insert the block. + # It's okay to autoblock. Go ahead and insert/update the block... - $ipblock = Block::newFromDB( $autoblockIP ); + # Do not add a *new* block if the IP is already blocked. + $ipblock = Block::newFromTarget( $autoblockIP ); if ( $ipblock ) { - # If the user is already blocked. Then check if the autoblock would - # exceed the user block. If it would exceed, then do nothing, else - # prolong block time - if ( $this->mExpiry && - ( $this->mExpiry < Block::getAutoblockExpiry( $ipblock->mTimestamp ) ) + # Check if the block is an autoblock and would exceed the user block + # if renewed. If so, do nothing, otherwise prolong the block time... + if ( $ipblock->mAuto && // @TODO: why not compare $ipblock->mExpiry? + $this->mExpiry > Block::getAutoblockExpiry( $ipblock->mTimestamp ) ) { - return; - } - - # Just update the timestamp - if ( !$justInserted ) { + # Reset block timestamp to now and its expiry to + # $wgAutoblockExpiry in the future $ipblock->updateTimestamp(); } + return false; + } - return; - } else { - $ipblock = new Block; - } - - # Make a new block object with the desired properties - wfDebug( "Autoblocking {$this->mAddress}@" . $autoblockIP . "\n" ); - $ipblock->mAddress = $autoblockIP; - $ipblock->mUser = 0; - $ipblock->mBy = $this->mBy; - $ipblock->mByName = $this->mByName; - $ipblock->mReason = wfMsgForContent( 'autoblocker', $this->mAddress, $this->mReason ); - $ipblock->mTimestamp = wfTimestampNow(); - $ipblock->mAuto = 1; - $ipblock->mCreateAccount = $this->mCreateAccount; + # Make a new block object with the desired properties. + $autoblock = new Block; + wfDebug( "Autoblocking {$this->getTarget()}@" . $autoblockIP . "\n" ); + $autoblock->setTarget( $autoblockIP ); + $autoblock->setBlocker( $this->getBlocker() ); + $autoblock->mReason = wfMsgForContent( 'autoblocker', $this->getTarget(), $this->mReason ); + $timestamp = wfTimestampNow(); + $autoblock->mTimestamp = $timestamp; + $autoblock->mAuto = 1; + $autoblock->prevents( 'createaccount', $this->prevents( 'createaccount' ) ); # Continue suppressing the name if needed - $ipblock->mHideName = $this->mHideName; - $ipblock->mAllowUsertalk = $this->mAllowUsertalk; + $autoblock->mHideName = $this->mHideName; + $autoblock->prevents( 'editownusertalk', $this->prevents( 'editownusertalk' ) ); - # If the user is already blocked with an expiry date, we don't - # want to pile on top of that! - if ( $this->mExpiry ) { - $ipblock->mExpiry = min( $this->mExpiry, Block::getAutoblockExpiry( $this->mTimestamp ) ); + if ( $this->mExpiry == 'infinity' ) { + # Original block was indefinite, start an autoblock now + $autoblock->mExpiry = Block::getAutoblockExpiry( $timestamp ); } else { - $ipblock->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp ); + # If the user is already blocked with an expiry date, we don't + # want to pile on top of that. + $autoblock->mExpiry = min( $this->mExpiry, Block::getAutoblockExpiry( $timestamp ) ); } - # Insert it - return $ipblock->insert(); + # Insert the block... + $status = $autoblock->insert(); + return $status + ? $status['id'] + : false; } /** @@ -681,12 +684,13 @@ class Block { * @return Boolean */ public function isExpired() { - wfDebug( "Block::isExpired() checking current " . wfTimestampNow() . " vs $this->mExpiry\n" ); + $timestamp = wfTimestampNow(); + wfDebug( "Block::isExpired() checking current " . $timestamp . " vs $this->mExpiry\n" ); if ( !$this->mExpiry ) { return false; } else { - return wfTimestampNow() > $this->mExpiry; + return $timestamp > $this->mExpiry; } } @@ -695,7 +699,7 @@ class Block { * @return Boolean */ public function isValid() { - return $this->mAddress != ''; + return $this->getTarget() != null; } /** @@ -711,20 +715,58 @@ class Block { array( /* SET */ 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ), 'ipb_expiry' => $dbw->timestamp( $this->mExpiry ), - ), array( /* WHERE */ - 'ipb_address' => $this->mAddress - ), 'Block::updateTimestamp' + ), + array( /* WHERE */ + 'ipb_address' => (string)$this->getTarget() + ), + __METHOD__ ); } } /** + * Get the IP address at the start of the range in Hex form + * @return String IP in Hex form + */ + public function getRangeStart() { + switch( $this->type ) { + case self::TYPE_USER: + return ''; + case self::TYPE_IP: + return IP::toHex( $this->target ); + case self::TYPE_RANGE: + list( $start, /*...*/ ) = IP::parseRange( $this->target ); + return $start; + default: throw new MWException( "Block with invalid type" ); + } + } + + /** + * Get the IP address at the start of the range in Hex form + * @return String IP in Hex form + */ + public function getRangeEnd() { + switch( $this->type ) { + case self::TYPE_USER: + return ''; + case self::TYPE_IP: + return IP::toHex( $this->target ); + case self::TYPE_RANGE: + list( /*...*/, $end ) = IP::parseRange( $this->target ); + return $end; + default: throw new MWException( "Block with invalid type" ); + } + } + + /** * Get the user id of the blocking sysop * * @return Integer */ public function getBy() { - return $this->mBy; + return $this->getBlocker() instanceof User + ? $this->getBlocker()->getId() + : 0; } /** @@ -733,32 +775,102 @@ class Block { * @return String */ public function getByName() { - return $this->mByName; + return $this->getBlocker() instanceof User + ? $this->getBlocker()->getName() + : null; + } + + /** + * Get the block ID + * @return int + */ + public function getId() { + return $this->mId; } /** * Get/set the SELECT ... FOR UPDATE flag + * @deprecated since 1.18 + * + * @param $x Bool */ public function forUpdate( $x = null ) { - return wfSetVar( $this->mForUpdate, $x ); + # noop } /** * Get/set a flag determining whether the master is used for reads + * + * @param $x Bool + * @return Bool */ public function fromMaster( $x = null ) { return wfSetVar( $this->mFromMaster, $x ); } /** + * Get/set whether the Block is a hardblock (affects logged-in users on a given IP/range + * @param $x Bool + * @return Bool + */ + public function isHardblock( $x = null ) { + wfSetVar( $this->isHardblock, $x ); + + # You can't *not* hardblock a user + return $this->getType() == self::TYPE_USER + ? true + : $this->isHardblock; + } + + public function isAutoblocking( $x = null ) { + wfSetVar( $this->isAutoblocking, $x ); + + # You can't put an autoblock on an IP or range as we don't have any history to + # look over to get more IPs from + return $this->getType() == self::TYPE_USER + ? $this->isAutoblocking + : false; + } + + /** + * Get/set whether the Block prevents a given action + * @param $action String + * @param $x Bool + * @return Bool + */ + public function prevents( $action, $x = null ) { + switch( $action ) { + case 'edit': + # For now... <evil laugh> + return true; + + case 'createaccount': + return wfSetVar( $this->mCreateAccount, $x ); + + case 'sendemail': + return wfSetVar( $this->mBlockEmail, $x ); + + case 'editownusertalk': + return wfSetVar( $this->mDisableUsertalk, $x ); + + default: + return null; + } + } + + /** * Get the block name, but with autoblocked IPs hidden as per standard privacy policy - * @return String + * @return String, text is escaped */ public function getRedactedName() { if ( $this->mAuto ) { - return '#' . $this->mId; + return Html::rawElement( + 'span', + array( 'class' => 'mw-autoblockid' ), + wfMessage( 'autoblockid', $this->mId ) + ); } else { - return $this->mAddress; + return htmlspecialchars( $this->getTarget() ); } } @@ -768,33 +880,29 @@ class Block { * @param $expiry String: timestamp for expiry, or * @param $db Database object * @return String + * @deprecated since 1.18; use $dbw->encodeExpiry() instead */ public static function encodeExpiry( $expiry, $db ) { - if ( $expiry == '' || $expiry == Block::infinity() ) { - return Block::infinity(); - } else { - return $db->timestamp( $expiry ); - } + return $db->encodeExpiry( $expiry ); } /** * Decode expiry which has come from the DB * * @param $expiry String: Database expiry format - * @param $timestampType Requested timestamp format + * @param $timestampType Int Requested timestamp format * @return String + * @deprecated since 1.18; use $wgLang->decodeExpiry() instead */ public static function decodeExpiry( $expiry, $timestampType = TS_MW ) { - if ( $expiry == '' || $expiry == Block::infinity() ) { - return Block::infinity(); - } else { - return wfTimestamp( $timestampType, $expiry ); - } + global $wgContLang; + return $wgContLang->formatExpiry( $expiry, $timestampType ); } /** * Get a timestamp of the expiry for autoblocks * + * @param $timestamp String|Int * @return String */ public static function getAutoblockExpiry( $timestamp ) { @@ -808,35 +916,10 @@ class Block { * For example, 127.111.113.151/24 -> 127.111.113.0/24 * @param $range String: IP address to normalize * @return string + * @deprecated since 1.18, call IP::sanitizeRange() directly */ public static function normaliseRange( $range ) { - $parts = explode( '/', $range ); - if ( count( $parts ) == 2 ) { - // IPv6 - if ( IP::isIPv6( $range ) && $parts[1] >= 64 && $parts[1] <= 128 ) { - $bits = $parts[1]; - $ipint = IP::toUnsigned( $parts[0] ); - # Native 32 bit functions WON'T work here!!! - # Convert to a padded binary number - $network = wfBaseConvert( $ipint, 10, 2, 128 ); - # Truncate the last (128-$bits) bits and replace them with zeros - $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT ); - # Convert back to an integer - $network = wfBaseConvert( $network, 2, 10 ); - # Reform octet address - $newip = IP::toOctet( $network ); - $range = "$newip/{$parts[1]}"; - } // IPv4 - elseif ( IP::isIPv4( $range ) && $parts[1] >= 16 && $parts[1] <= 32 ) { - $shift = 32 - $parts[1]; - $ipint = IP::toUnsigned( $parts[0] ); - $ipint = $ipint >> $shift << $shift; - $newip = long2ip( $ipint ); - $range = "$newip/{$parts[1]}"; - } - } - - return $range; + return IP::sanitizeRange( $range ); } /** @@ -849,25 +932,12 @@ class Block { /** * Get a value to insert into expiry field of the database when infinite expiry - * is desired. In principle this could be DBMS-dependant, but currently all - * supported DBMS's support the string "infinity", so we just use that. - * + * is desired + * @deprecated since 1.18, call $dbr->getInfinity() directly * @return String */ public static function infinity() { - # This is a special keyword for timestamps in PostgreSQL, and - # works with CHAR(14) as well because "i" sorts after all numbers. - - # BEGIN DatabaseMssql hack - # Since MSSQL doesn't recognize the infinity keyword, set date manually. - # TO-DO: Refactor for better DB portability and remove magic date - $dbr = wfGetDB( DB_SLAVE ); - if ( $dbr->getType() == 'mssql' ) { - return '3000-01-31 00:00:00.000'; - } - # End DatabaseMssql hack - - return 'infinity'; + return wfGetDB( DB_SLAVE )->getInfinity(); } /** @@ -875,8 +945,10 @@ class Block { * * @param $encoded_expiry String: Database encoded expiry time * @return Html-escaped String + * @deprecated since 1.18; use $wgLang->formatExpiry() instead */ public static function formatExpiry( $encoded_expiry ) { + global $wgContLang; static $msg = null; if ( is_null( $msg ) ) { @@ -888,8 +960,8 @@ class Block { } } - $expiry = Block::decodeExpiry( $encoded_expiry ); - if ( $expiry == 'infinity' ) { + $expiry = $wgContLang->formatExpiry( $encoded_expiry, TS_MW ); + if ( $expiry == wfGetDB( DB_SLAVE )->getInfinity() ) { $expirystr = $msg['infiniteblock']; } else { global $wgLang; @@ -902,21 +974,179 @@ class Block { } /** - * Convert a typed-in expiry time into something we can put into the database. - * @param $expiry_input String: whatever was typed into the form - * @return String: more database friendly + * Convert a submitted expiry time, which may be relative ("2 weeks", etc) or absolute + * ("24 May 2034"), into an absolute timestamp we can put into the database. + * @param $expiry String: whatever was typed into the form + * @return String: timestamp or "infinity" string for th DB implementation + * @deprecated since 1.18 moved to SpecialBlock::parseExpiryInput() */ - public static function parseExpiryInput( $expiry_input ) { - if ( $expiry_input == 'infinite' || $expiry_input == 'indefinite' ) { - $expiry = 'infinity'; + public static function parseExpiryInput( $expiry ) { + wfDeprecated( __METHOD__ ); + return SpecialBlock::parseExpiryInput( $expiry ); + } + + /** + * Given a target and the target's type, get an existing Block object if possible. + * @param $specificTarget String|User|Int a block target, which may be one of several types: + * * A user to block, in which case $target will be a User + * * An IP to block, in which case $target will be a User generated by using + * User::newFromName( $ip, false ) to turn off name validation + * * An IP range, in which case $target will be a String "123.123.123.123/18" etc + * * The ID of an existing block, in the format "#12345" (since pure numbers are valid + * usernames + * Calling this with a user, IP address or range will not select autoblocks, and will + * only select a block where the targets match exactly (so looking for blocks on + * 1.2.3.4 will not select 1.2.0.0/16 or even 1.2.3.4/32) + * @param $vagueTarget String|User|Int as above, but we will search for *any* block which + * affects that target (so for an IP address, get ranges containing that IP; and also + * get any relevant autoblocks). Leave empty or blank to skip IP-based lookups. + * @param $fromMaster Bool whether to use the DB_MASTER database + * @return Block|null (null if no relevant block could be found). The target and type + * of the returned Block will refer to the actual block which was found, which might + * not be the same as the target you gave if you used $vagueTarget! + */ + public static function newFromTarget( $specificTarget, $vagueTarget = null, $fromMaster = false ) { + + list( $target, $type ) = self::parseTarget( $specificTarget ); + if( $type == Block::TYPE_ID || $type == Block::TYPE_AUTO ){ + return Block::newFromID( $target ); + + } elseif( $target === null && $vagueTarget == '' ){ + # We're not going to find anything useful here + # Be aware that the == '' check is explicit, since empty values will be + # passed by some callers (bug 29116) + return null; + + } elseif( in_array( $type, array( Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE, null ) ) ) { + $block = new Block(); + $block->fromMaster( $fromMaster ); + + if( $type !== null ){ + $block->setTarget( $target ); + } + + if( $block->newLoad( $vagueTarget ) ){ + return $block; + } else { + return null; + } } else { - $expiry = strtotime( $expiry_input ); + return null; + } + } - if ( $expiry < 0 || $expiry === false ) { - return false; + /** + * From an existing Block, get the target and the type of target. Note that it is + * always safe to treat the target as a string; for User objects this will return + * User::__toString() which in turn gives User::getName(). + * + * @param $target String|Int|User + * @return array( User|String, Block::TYPE_ constant ) + */ + public static function parseTarget( $target ) { + $target = trim( $target ); + + # We may have been through this before + if( $target instanceof User ){ + if( IP::isValid( $target->getName() ) ){ + return array( $target, self::TYPE_IP ); + } else { + return array( $target, self::TYPE_USER ); } + } elseif( $target === null ){ + return array( null, null ); + } + + if ( IP::isValid( $target ) ) { + # We can still create a User if it's an IP address, but we need to turn + # off validation checking (which would exclude IP addresses) + return array( + User::newFromName( IP::sanitizeIP( $target ), false ), + Block::TYPE_IP + ); + + } elseif ( IP::isValidBlock( $target ) ) { + # Can't create a User from an IP range + return array( IP::sanitizeRange( $target ), Block::TYPE_RANGE ); } - return $expiry; + # Consider the possibility that this is not a username at all + # but actually an old subpage (bug #29797) + if( strpos( $target, '/' ) !== false ){ + # An old subpage, drill down to the user behind it + $parts = explode( '/', $target ); + $target = $parts[0]; + } + + $userObj = User::newFromName( $target ); + if ( $userObj instanceof User ) { + # Note that since numbers are valid usernames, a $target of "12345" will be + # considered a User. If you want to pass a block ID, prepend a hash "#12345", + # since hash characters are not valid in usernames or titles generally. + return array( $userObj, Block::TYPE_USER ); + + } elseif ( preg_match( '/^#\d+$/', $target ) ) { + # Autoblock reference in the form "#12345" + return array( substr( $target, 1 ), Block::TYPE_AUTO ); + + } else { + # WTF? + return array( null, null ); + } + } + + /** + * Get the type of target for this particular block + * @return Block::TYPE_ constant, will never be TYPE_ID + */ + public function getType() { + return $this->mAuto + ? self::TYPE_AUTO + : $this->type; + } + + /** + * Get the target and target type for this particular Block. Note that for autoblocks, + * this returns the unredacted name; frontend functions need to call $block->getRedactedName() + * in this situation. + * @return array( User|String, Block::TYPE_ constant ) + * @todo FIXME: This should be an integral part of the Block member variables + */ + public function getTargetAndType() { + return array( $this->getTarget(), $this->getType() ); + } + + /** + * Get the target for this particular Block. Note that for autoblocks, + * this returns the unredacted name; frontend functions need to call $block->getRedactedName() + * in this situation. + * @return User|String + */ + public function getTarget() { + return $this->target; + } + + /** + * Set the target for this block, and update $this->type accordingly + * @param $target Mixed + */ + public function setTarget( $target ){ + list( $this->target, $this->type ) = self::parseTarget( $target ); + } + + /** + * Get the user who implemented this block + * @return User + */ + public function getBlocker(){ + return $this->blocker; + } + + /** + * Set the user who implemented (or will implement) this block + * @param $user User + */ + public function setBlocker( User $user ){ + $this->blocker = $user; } } diff --git a/includes/Category.php b/includes/Category.php index 614933ff..9d9b5a67 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -13,7 +13,10 @@ class Category { /** Name of the category, normalized to DB-key form */ private $mName = null; private $mID = null; - /** Category page title */ + /** + * Category page title + * @var Title + */ private $mTitle = null; /** Counts of membership (cat_pages, cat_subcats, cat_files) */ private $mPages = null, $mSubcats = null, $mFiles = null; @@ -100,7 +103,7 @@ class Category { * Factory function. * * @param $title Title for the category page - * @return Mixed: category, or false on a totally invalid name + * @return category|false on a totally invalid name */ public static function newFromTitle( $title ) { $cat = new self(); @@ -129,7 +132,7 @@ class Category { * @param $row result set row, must contain the cat_xxx fields. If the fields are null, * the resulting Category object will represent an empty category if a title object * was given. If the fields are null and no title was given, this method fails and returns false. - * @param $title optional title object for the category represented by the given row. + * @param Title $title optional title object for the category represented by the given row. * May be provided if it is already known, to avoid having to re-create a title object later. * @return Category */ @@ -182,7 +185,7 @@ class Category { public function getFileCount() { return $this->getX( 'mFiles' ); } /** - * @return mixed The Title for this category, or false on failure. + * @return Title|false Title for this category, or false on failure. */ public function getTitle() { if ( $this->mTitle ) return $this->mTitle; diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index f990b79b..6a0f6132 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -1,6 +1,6 @@ <?php /** - * Special handling for category description pages. + * Class for viewing MediaWiki category description pages. * Modelled after ImagePage.php. * * @file @@ -17,6 +17,22 @@ class CategoryPage extends Article { # Subclasses can change this to override the viewer class. protected $mCategoryViewerClass = 'CategoryViewer'; + protected function newPage( Title $title ) { + // Overload mPage with a category-specific page + return new WikiCategoryPage( $title ); + } + + /** + * Constructor from a page id + * @param $id Int article ID to load + */ + 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() { global $wgRequest, $wgUser; @@ -42,27 +58,6 @@ class CategoryPage extends Article { } } - /** - * Don't return a 404 for categories in use. - * In use defined as: either the actual page exists - * or the category currently has members. - */ - 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; - } - function openShowCategory() { # For overloading } @@ -70,27 +65,77 @@ class CategoryPage extends Article { function closeShowCategory() { global $wgOut, $wgRequest; + // Use these as defaults for back compat --catrope + $oldFrom = $wgRequest->getVal( 'from' ); + $oldUntil = $wgRequest->getVal( 'until' ); + + $reqArray = $wgRequest->getValues(); + $from = $until = array(); foreach ( array( 'page', 'subcat', 'file' ) as $type ) { - $from[$type] = $wgRequest->getVal( "{$type}from" ); - $until[$type] = $wgRequest->getVal( "{$type}until" ); + $from[$type] = $wgRequest->getVal( "{$type}from", $oldFrom ); + $until[$type] = $wgRequest->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"]; + } } - $viewer = new $this->mCategoryViewerClass( $this->mTitle, $from, $until, $wgRequest->getValues() ); + unset( $reqArray["from"] ); + unset( $reqArray["to"] ); + + $viewer = new $this->mCategoryViewerClass( $this->mTitle, $from, $until, $reqArray ); $wgOut->addHTML( $viewer->getHTML() ); } } class CategoryViewer { - var $title, $limit, $from, $until, + var $limit, $from, $until, $articles, $articles_start_char, $children, $children_start_char, - $showGallery, $gallery, - $imgsNoGalley, $imgsNoGallery_start_char, - $skin, $collation; - # Category object for this page + $showGallery, $imgsNoGalley, + $imgsNoGallery_start_char, + $imgsNoGallery; + + /** + * @var + */ + var $nextPage; + + /** + * @var Array + */ + var $flip; + + /** + * @var Title + */ + var $title; + + /** + * @var Collation + */ + var $collation; + + /** + * @var ImageGallery + */ + var $gallery; + + /** + * Category object for this page + * @var Category + */ private $cat; - # The original query array, to be used in generating paging links. + + /** + * The original query array, to be used in generating paging links. + * @var array + */ private $query; function __construct( $title, $from = '', $until = '', $query = array() ) { @@ -111,7 +156,7 @@ class CategoryViewer { * @return string HTML output */ public function getHTML() { - global $wgOut, $wgCategoryMagicGallery, $wgContLang; + global $wgOut, $wgCategoryMagicGallery, $wgLang, $wgContLang; wfProfileIn( __METHOD__ ); $this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery; @@ -127,7 +172,7 @@ class CategoryViewer { if ( $r == '' ) { // If there is no category content to display, only // show the top part of the navigation links. - // FIXME: cannot be completely suppressed because it + // @todo FIXME: Cannot be completely suppressed because it // is unknown if 'until' or 'from' makes this // give 0 results. $r = $r . $this->getCategoryTop(); @@ -142,6 +187,12 @@ class CategoryViewer { $r = wfMsgExt( 'category-empty', array( 'parse' ) ); } + $pageLang = $this->title->getPageLanguage(); + $langAttribs = array( 'lang' => $wgLang->getCode(), 'dir' => $wgLang->getDir() ); + # close the previous div, show the headings in user language, + # then open a new div with the page content language again + $r = Html::openElement( 'div', $langAttribs ) . $r . '</div>'; + wfProfileOut( __METHOD__ ); return $wgContLang->convert( $r ); } @@ -160,14 +211,6 @@ class CategoryViewer { } } - function getSkin() { - if ( !$this->skin ) { - global $wgUser; - $this->skin = $wgUser->getSkin(); - } - return $this->skin; - } - /** * Add a subcategory to the internal lists, using a Category object */ @@ -175,7 +218,7 @@ class CategoryViewer { // Subcategory; strip the 'Category' namespace from the link text. $title = $cat->getTitle(); - $link = $this->getSkin()->link( $title, $title->getText() ); + $link = Linker::link( $title, htmlspecialchars( $title->getText() ) ); if ( $title->isRedirect() ) { // This didn't used to add redirect-in-category, but might // as well be consistent with the rest of the sections @@ -190,7 +233,7 @@ class CategoryViewer { /** * Add a subcategory to the internal lists, using a title object - * @deprecated kept for compatibility, please use addSubcategoryObject instead + * @deprecated since 1.17 kept for compatibility, please use addSubcategoryObject instead */ function addSubcategory( Title $title, $sortkey, $pageLength ) { $this->addSubcategoryObject( Category::newFromTitle( $title ), $sortkey, $pageLength ); @@ -233,7 +276,7 @@ class CategoryViewer { $this->gallery->add( $title ); } } else { - $link = $this->getSkin()->link( $title ); + $link = Linker::link( $title ); if ( $isRedirect ) { // This seems kind of pointless given 'mw-redirect' class, // but keeping for back-compatibility with user css. @@ -252,7 +295,7 @@ class CategoryViewer { function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { global $wgContLang; - $link = $this->getSkin()->link( $title ); + $link = Linker::link( $title ); if ( $isRedirect ) { // This seems kind of pointless given 'mw-redirect' class, // but keeping for back-compatiability with user css. @@ -309,7 +352,7 @@ class CategoryViewer { 'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files', 'cl_sortkey_prefix', 'cl_collation' ), - array( 'cl_to' => $this->title->getDBkey() ) + $extraConds, + array_merge( array( 'cl_to' => $this->title->getDBkey() ), $extraConds ), __METHOD__, array( 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ), @@ -385,7 +428,7 @@ class CategoryViewer { # Don't show articles section if there are none. $r = ''; - # FIXME, here and in the other two sections: we don't need to bother + # @todo FIXME: Here and in the other two sections: we don't need to bother # with this rigamarole if the entire category contents fit on one page # and have already been retrieved. We can just use $rescnt in that # case and save a query and some logic. @@ -460,13 +503,20 @@ class CategoryViewer { * @private */ function formatList( $articles, $articles_start_char, $cutoff = 6 ) { + $list = ''; if ( count ( $articles ) > $cutoff ) { - return self::columnList( $articles, $articles_start_char ); + $list = self::columnList( $articles, $articles_start_char ); } elseif ( count( $articles ) > 0 ) { // for short lists of articles in categories. - return self::shortList( $articles, $articles_start_char ); + $list = self::shortList( $articles, $articles_start_char ); } - return ''; + + $pageLang = $this->title->getPageLanguage(); + $attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), + 'class' => 'mw-content-'.$pageLang->getDir() ); + $list = Html::rawElement( 'div', $attribs, $list ); + + return $list; } /** @@ -539,10 +589,8 @@ class CategoryViewer { static function shortList( $articles, $articles_start_char ) { $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n"; $r .= '<ul><li>' . $articles[0] . '</li>'; - for ( $index = 1; $index < count( $articles ); $index++ ) - { - if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) - { + for ( $index = 1; $index < count( $articles ); $index++ ) { + if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) { $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>"; } @@ -563,7 +611,7 @@ class CategoryViewer { */ private function pagingLinks( $first, $last, $type = '' ) { global $wgLang; - $sk = $this->getSkin(); + $limitText = $wgLang->formatNum( $this->limit ); $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText ); @@ -572,8 +620,8 @@ class CategoryViewer { $prevQuery = $this->query; $prevQuery["{$type}until"] = $first; unset( $prevQuery["{$type}from"] ); - $prevLink = $sk->linkKnown( - $this->title, + $prevLink = Linker::linkKnown( + $this->addFragmentToTitle( $this->title, $type ), $prevLink, array(), $prevQuery @@ -586,8 +634,8 @@ class CategoryViewer { $lastQuery = $this->query; $lastQuery["{$type}from"] = $last; unset( $lastQuery["{$type}until"] ); - $nextLink = $sk->linkKnown( - $this->title, + $nextLink = Linker::linkKnown( + $this->addFragmentToTitle( $this->title, $type ), $nextLink, array(), $lastQuery @@ -598,8 +646,34 @@ class CategoryViewer { } /** + * Takes a title, and adds the fragment identifier that + * corresponds to the correct segment of the category. + * + * @param Title $title: The title (usually $this->title) + * @param String $section: Which section + */ + private function addFragmentToTitle( $title, $section ) { + switch ( $section ) { + case 'page': + $fragment = 'mw-pages'; + break; + case 'subcat': + $fragment = 'mw-subcategories'; + break; + case 'file': + $fragment = 'mw-category-media'; + break; + default: + throw new MWException( __METHOD__ . + " Invalid section $section." ); + } + + return Title::makeTitle( $title->getNamespace(), + $title->getDBkey(), $fragment ); + } + /** * What to do if the category table conflicts with the number of results - * returned? This function says what. Each type is considered independantly + * returned? This function says what. Each type is considered independently * of the other types. * * Note for grepping: uses the messages category-article-count, @@ -640,8 +714,7 @@ class CategoryViewer { } if ( $dbcnt == $rescnt || ( ( $rescnt == $this->limit || $fromOrUntil ) - && $dbcnt > $rescnt ) ) - { + && $dbcnt > $rescnt ) ) { # Case 1: seems sane. $totalcnt = $dbcnt; } elseif ( $rescnt < $this->limit && !$fromOrUntil ) { diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php index 1f08b7f8..2567de0d 100644 --- a/includes/Categoryfinder.php +++ b/includes/Categoryfinder.php @@ -42,6 +42,7 @@ class Categoryfinder { * @param $article_ids Array of article IDs * @param $categories FIXME * @param $mode String: FIXME, default 'AND'. + * @todo FIXME: $categories/$mode */ function seed( $article_ids, $categories, $mode = 'AND' ) { $this->articles = $article_ids; @@ -85,9 +86,9 @@ class Categoryfinder { /** * This functions recurses through the parent representation, trying to match the conditions - * @param $id The article/category to check - * @param $conds The array of categories to match - * @param $path used to check for recursion loops + * @param $id int The article/category to check + * @param $conds array The array of categories to match + * @param $path array used to check for recursion loops * @return bool Does this match the conditions? */ function check( $id, &$conds, $path = array() ) { diff --git a/includes/Cdb.php b/includes/Cdb.php index 60477485..d7a2bca5 100644 --- a/includes/Cdb.php +++ b/includes/Cdb.php @@ -13,6 +13,10 @@ abstract class CdbReader { /** * Open a file and return a subclass instance + * + * @param $fileName string + * + * @return CdbReader */ public static function open( $fileName ) { if ( self::haveExtension() ) { @@ -25,6 +29,8 @@ abstract class CdbReader { /** * Returns true if the native extension is available + * + * @return bool */ public static function haveExtension() { if ( !function_exists( 'dba_handlers' ) ) { @@ -49,6 +55,8 @@ abstract class CdbReader { /** * Get a value with a given key. Only string values are supported. + * + * @param $key string */ abstract public function get( $key ); } @@ -61,6 +69,10 @@ abstract class CdbWriter { /** * Open a writer and return a subclass instance. * The user must have write access to the directory, for temporary file creation. + * + * @param $fileName string + * + * @return bool */ public static function open( $fileName ) { if ( CdbReader::haveExtension() ) { diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php index 1485cc66..f4029ba5 100644 --- a/includes/Cdb_PHP.php +++ b/includes/Cdb_PHP.php @@ -16,6 +16,11 @@ class CdbFunctions { /** * Take a modulo of a signed integer as if it were an unsigned integer. * $b must be less than 0x40000000 and greater than 0 + * + * @param $a + * @param $b + * + * @return int */ public static function unsignedMod( $a, $b ) { if ( $a & 0x80000000 ) { @@ -25,9 +30,12 @@ class CdbFunctions { return $a % $b; } } - + /** * Shift a signed integer right as if it were unsigned + * @param $a + * @param $b + * @return int */ public static function unsignedShiftRight( $a, $b ) { if ( $b == 0 ) { @@ -42,6 +50,10 @@ class CdbFunctions { /** * The CDB hash function. + * + * @param $s + * + * @return */ public static function hash( $s ) { $h = 5381; @@ -103,11 +115,16 @@ class CdbReader_PHP extends CdbReader { } function close() { - if( isset($this->handle) ) + if( isset( $this->handle ) ) { fclose( $this->handle ); + } unset( $this->handle ); } + /** + * @param $key + * @return bool|string + */ public function get( $key ) { // strval is required if ( $this->find( strval( $key ) ) ) { @@ -117,6 +134,11 @@ class CdbReader_PHP extends CdbReader { } } + /** + * @param $key + * @param $pos + * @return bool + */ protected function match( $key, $pos ) { $buf = $this->read( strlen( $key ), $pos ); return $buf === $key; @@ -126,6 +148,12 @@ class CdbReader_PHP extends CdbReader { $this->loop = 0; } + /** + * @throws MWException + * @param $length + * @param $pos + * @return string + */ protected function read( $length, $pos ) { if ( fseek( $this->handle, $pos ) == -1 ) { // This can easily happen if the internal pointers are incorrect @@ -145,6 +173,8 @@ class CdbReader_PHP extends CdbReader { /** * Unpack an unsigned integer and throw an exception if it needs more than 31 bits + * @param $s + * @return */ protected function unpack31( $s ) { $data = unpack( 'V', $s ); @@ -156,12 +186,18 @@ class CdbReader_PHP extends CdbReader { /** * Unpack a 32-bit signed integer + * @param $s + * @return int */ protected function unpackSigned( $s ) { $data = unpack( 'va/vb', $s ); return $data['a'] | ( $data['b'] << 16 ); } + /** + * @param $key + * @return bool + */ protected function findNext( $key ) { if ( !$this->loop ) { $u = CdbFunctions::hash( $key ); @@ -204,6 +240,10 @@ class CdbReader_PHP extends CdbReader { return false; } + /** + * @param $key + * @return bool + */ protected function find( $key ) { $this->findStart(); return $this->findNext( $key ); @@ -240,6 +280,11 @@ class CdbWriter_PHP extends CdbWriter { } } + /** + * @param $key + * @param $value + * @return + */ public function set( $key, $value ) { if ( strval( $key ) === '' ) { // DBA cross-check hack @@ -251,10 +296,14 @@ class CdbWriter_PHP extends CdbWriter { $this->addend( strlen( $key ), strlen( $value ), CdbFunctions::hash( $key ) ); } + /** + * @throws MWException + */ public function close() { $this->finish(); - if( isset($this->handle) ) + if( isset($this->handle) ) { fclose( $this->handle ); + } if ( wfIsWindows() && file_exists($this->realFileName) ) { unlink( $this->realFileName ); } @@ -264,6 +313,10 @@ class CdbWriter_PHP extends CdbWriter { unset( $this->handle ); } + /** + * @throws MWException + * @param $buf + */ protected function write( $buf ) { $len = fwrite( $this->handle, $buf ); if ( $len !== strlen( $buf ) ) { @@ -271,6 +324,10 @@ class CdbWriter_PHP extends CdbWriter { } } + /** + * @throws MWException + * @param $len + */ protected function posplus( $len ) { $newpos = $this->pos + $len; if ( $newpos > 0x7fffffff ) { @@ -279,6 +336,11 @@ class CdbWriter_PHP extends CdbWriter { $this->pos = $newpos; } + /** + * @param $keylen + * @param $datalen + * @param $h + */ protected function addend( $keylen, $datalen, $h ) { $this->hplist[] = array( 'h' => $h, @@ -291,6 +353,11 @@ class CdbWriter_PHP extends CdbWriter { $this->posplus( $datalen ); } + /** + * @throws MWException + * @param $keylen + * @param $datalen + */ protected function addbegin( $keylen, $datalen ) { if ( $keylen > 0x7fffffff ) { throw new MWException( __METHOD__.': key length too long' ); @@ -302,6 +369,9 @@ class CdbWriter_PHP extends CdbWriter { $this->write( $buf ); } + /** + * @throws MWException + */ protected function finish() { // Hack for DBA cross-check $this->hplist = array_reverse( $this->hplist ); diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php index 7f0fee21..c8e522df 100644 --- a/includes/ChangeTags.php +++ b/includes/ChangeTags.php @@ -1,8 +1,5 @@ <?php -if( !defined( 'MEDIAWIKI' ) ) - die; - class ChangeTags { static function formatSummaryRow( $tags, $page ) { if( !$tags ) @@ -28,11 +25,8 @@ class ChangeTags { } static function tagDescription( $tag ) { - $msg = wfMsgExt( "tag-$tag", 'parseinline' ); - if ( wfEmptyMsg( "tag-$tag", $msg ) ) { - return htmlspecialchars( $tag ); - } - return $msg; + $msg = wfMessage( "tag-$tag" ); + return $msg->exists() ? $msg->parse() : htmlspecialchars( $tag ); } ## Basic utility method to add tags to a particular change, given its rc_id, rev_id and/or log_id. @@ -150,18 +144,26 @@ class ChangeTags { } /** - * If $fullForm is set to false, then it returns an array of (label, form). - * If $fullForm is true, it returns an entire form. + * Build a text box to select a change tag + * + * @param $selected String: tag to select by default + * @param $fullForm Boolean: + * - if false, then it returns an array of (label, form). + * - if true, it returns an entire form around the selector. + * @param $title Title object to send the form to. + * Used when, and only when $fullForm is true. + * @return String or array: + * - if $fullForm is false: Array with + * - if $fullForm is true: String, html fragment */ - static function buildTagFilterSelector( $selected='', $fullForm = false /* used to put a full form around the selector */ ) { + public static function buildTagFilterSelector( $selected='', $fullForm = false, Title $title = null ) { global $wgUseTagFilter; if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) return $fullForm ? '' : array(); - global $wgTitle; - - $data = array( wfMsgExt( 'tag-filter', 'parseinline' ), Xml::input( 'tagfilter', 20, $selected ) ); + $data = array( Html::rawElement( 'label', array( 'for' => 'tagfilter' ), wfMsgExt( 'tag-filter', 'parseinline' ) ), + Xml::input( 'tagfilter', 20, $selected ) ); if ( !$fullForm ) { return $data; @@ -175,7 +177,11 @@ class ChangeTags { return $html; } - /** Basically lists defined tags which count even if they aren't applied to anything */ + /** + *Basically lists defined tags which count even if they aren't applied to anything + * + * @return array + */ static function listDefinedTags() { // Caching... global $wgMemc; diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php index f07b6505..c4c4a8a1 100644 --- a/includes/ChangesFeed.php +++ b/includes/ChangesFeed.php @@ -24,15 +24,19 @@ class ChangesFeed { * * @param $title String: feed's title * @param $description String: feed's description + * @param $url String: url of origin page * @return ChannelFeed subclass or false on failure */ - public function getFeedObject( $title, $description ) { - global $wgSitename, $wgLanguageCode, $wgFeedClasses, $wgTitle; - $feedTitle = "$wgSitename - {$title} [$wgLanguageCode]"; - if( !isset($wgFeedClasses[$this->format] ) ) + public function getFeedObject( $title, $description, $url ) { + global $wgSitename, $wgLanguageCode, $wgFeedClasses; + + if ( !isset( $wgFeedClasses[$this->format] ) ) { return false; + } + + $feedTitle = "$wgSitename - {$title} [$wgLanguageCode]"; return new $wgFeedClasses[$this->format]( - $feedTitle, htmlspecialchars( $description ), $wgTitle->getFullUrl() ); + $feedTitle, htmlspecialchars( $description ), $url ); } /** @@ -57,11 +61,11 @@ class ChangesFeed { FeedUtils::checkPurge( $timekey, $key ); - /* - * Bumping around loading up diffs can be pretty slow, so where - * possible we want to cache the feed output so the next visitor - * gets it quick too. - */ + /** + * Bumping around loading up diffs can be pretty slow, so where + * possible we want to cache the feed output so the next visitor + * gets it quick too. + */ $cachedFeed = $this->loadFromCache( $lastmod, $timekey, $key ); if( is_string( $cachedFeed ) ) { wfDebug( "RC: Outputting cached feed\n" ); @@ -106,12 +110,12 @@ class ChangesFeed { $feedLastmod = $messageMemc->get( $timekey ); if( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) { - /* - * If the cached feed was rendered very recently, we may - * go ahead and use it even if there have been edits made - * since it was rendered. This keeps a swarm of requests - * from being too bad on a super-frequently edited wiki. - */ + /** + * If the cached feed was rendered very recently, we may + * go ahead and use it even if there have been edits made + * since it was rendered. This keeps a swarm of requests + * from being too bad on a super-frequently edited wiki. + */ $feedAge = time() - wfTimestamp( TS_UNIX, $feedLastmod ); $feedLastmodUnix = wfTimestamp( TS_UNIX, $feedLastmod ); @@ -145,6 +149,7 @@ class ChangesFeed { $n = 0; foreach( $rows as $obj ) { if( $n > 0 && + $obj->rc_type == RC_EDIT && $obj->rc_namespace >= 0 && $obj->rc_cur_id == $sorted[$n-1]->rc_cur_id && $obj->rc_user_text == $sorted[$n-1]->rc_user_text ) { @@ -157,16 +162,27 @@ class ChangesFeed { foreach( $sorted as $obj ) { $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title ); - $talkpage = $title->getTalkPage(); + $talkpage = MWNamespace::canTalk( $obj->rc_namespace ) ? $title->getTalkPage()->getFullUrl() : ''; // Skip items with deleted content (avoids partially complete/inconsistent output) if( $obj->rc_deleted ) continue; + + if ( $obj->rc_this_oldid ) { + $url = $title->getFullURL( + 'diff=' . $obj->rc_this_oldid . + '&oldid=' . $obj->rc_last_oldid + ); + } else { + // log entry or something like that. + $url = $title->getFullURL(); + } + $item = new FeedItem( $title->getPrefixedText(), FeedUtils::formatDiff( $obj ), - $obj->rc_this_oldid ? $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ) : $title->getFullURL(), + $url, $obj->rc_timestamp, ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $obj->rc_user_text, - $talkpage->getFullURL() + $talkpage ); $feed->outItem( $item ); } diff --git a/includes/ChangesList.php b/includes/ChangesList.php index b8bc4f55..1858dc3a 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -16,6 +16,10 @@ class RCCacheEntry extends RecentChange { var $curlink , $difflink, $lastlink, $usertalklink, $versionlink; var $userlink, $timestamp, $watched; + /** + * @param $rc RecentChange + * @return RCCacheEntry + */ static function newFromParent( $rc ) { $rc2 = new RCCacheEntry; $rc2->mAttribs = $rc->mAttribs; @@ -27,39 +31,64 @@ class RCCacheEntry extends RecentChange { /** * Base class for all changes lists */ -class ChangesList { +class ChangesList extends ContextSource { + + /** + * @var Skin + */ public $skin; + protected $watchlist = false; + protected $message; + /** - * Changeslist contructor - * @param $skin Skin - */ - public function __construct( $skin ) { - $this->skin = $skin; + * Changeslist contructor + * + * @param $obj Skin or IContextSource + */ + public function __construct( $obj ) { + if ( $obj instanceof IContextSource ) { + $this->setContext( $obj ); + $this->skin = $obj->getSkin(); + } else { + $this->setContext( $obj->getContext() ); + $this->skin = $obj; + } $this->preCacheMessages(); } /** - * Fetch an appropriate changes list class for the specified user - * Some users might want to use an enhanced list format, for instance + * Fetch an appropriate changes list class for the main context + * This first argument used to be an User object. * - * @param $user User to fetch the list class for - * @return ChangesList derivative + * @deprecated in 1.18; use newFromContext() instead + * @param $unused Unused + * @return ChangesList|EnhancedChangesList|OldChangesList derivative */ - public static function newFromUser( &$user ) { - global $wgRequest; + public static function newFromUser( $unused ) { + return self::newFromContext( RequestContext::getMain() ); + } - $sk = $user->getSkin(); + /** + * Fetch an appropriate changes list class for the specified context + * Some users might want to use an enhanced list format, for instance + * + * @param $context IContextSource to use + * @return ChangesList|EnhancedChangesList|OldChangesList derivative + */ + public static function newFromContext( IContextSource $context ) { + $user = $context->getUser(); + $sk = $context->getSkin(); $list = null; - if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) { - $new = $wgRequest->getBool( 'enhanced', $user->getOption( 'usenewrc' ) ); - return $new ? new EnhancedChangesList( $sk ) : new OldChangesList( $sk ); + if( wfRunHooks( 'FetchChangesList', array( $user, &$sk, &$list ) ) ) { + $new = $context->getRequest()->getBool( 'enhanced', $user->getOption( 'usenewrc' ) ); + return $new ? new EnhancedChangesList( $context ) : new OldChangesList( $context ); } else { return $list; } } - + /** * Sets the list to use a <li class="watchlist-(namespace)-(page)"> tag * @param $value Boolean @@ -81,21 +110,19 @@ class ChangesList { } } - /** * Returns the appropriate flags for new page, minor change and patrolling - * @param $new Boolean - * @param $minor Boolean - * @param $patrolled Boolean + * @param $flags Array Associative array of 'flag' => Bool * @param $nothing String to use for empty space - * @param $bot Boolean * @return String */ - protected function recentChangesFlags( $new, $minor, $patrolled, $nothing = ' ', $bot = false ) { - $f = $new ? self::flag( 'newpage' ) : $nothing; - $f .= $minor ? self::flag( 'minor' ) : $nothing; - $f .= $bot ? self::flag( 'bot' ) : $nothing; - $f .= $patrolled ? self::flag( 'unpatrolled' ) : $nothing; + protected function recentChangesFlags( $flags, $nothing = ' ' ) { + $f = ''; + foreach( array( 'newpage', 'minor', 'bot', 'unpatrolled' ) as $flag ){ + $f .= isset( $flags[$flag] ) && $flags[$flag] + ? self::flag( $flag ) + : $nothing; + } return $f; } @@ -105,28 +132,36 @@ class ChangesList { * unpatrolled edit. By default in English it will contain "N", "m", "b", * "!" respectively, plus it will have an appropriate title and class. * - * @param $key String: 'newpage', 'unpatrolled', 'minor', or 'bot' + * @param $flag String: 'newpage', 'unpatrolled', 'minor', or 'bot' * @return String: Raw HTML */ - public static function flag( $key ) { + public static function flag( $flag ) { static $messages = null; if ( is_null( $messages ) ) { - foreach ( explode( ' ', 'minoreditletter boteditletter newpageletter ' . - 'unpatrolledletter recentchanges-label-minor recentchanges-label-bot ' . - 'recentchanges-label-newpage recentchanges-label-unpatrolled' ) as $msg ) { - $messages[$msg] = wfMsgExt( $msg, 'escapenoentities' ); + $messages = array( + 'newpage' => array( 'newpageletter', 'recentchanges-label-newpage' ), + 'minoredit' => array( 'minoreditletter', 'recentchanges-label-minor' ), + 'botedit' => array( 'boteditletter', 'recentchanges-label-bot' ), + 'unpatrolled' => array( 'unpatrolledletter', 'recentchanges-label-unpatrolled' ), + ); + foreach( $messages as &$value ) { + $value[0] = wfMsgExt( $value[0], 'escapenoentities' ); + $value[1] = wfMsgExt( $value[1], 'escapenoentities' ); } } + # Inconsistent naming, bleh - if ( $key == 'newpage' || $key == 'unpatrolled' ) { - $key2 = $key; - } else { - $key2 = $key . 'edit'; - } - return "<abbr class=\"$key\" title=\"" - . $messages["recentchanges-label-$key"] . "\">" - . $messages["${key2}letter"] - . '</abbr>'; + $map = array( + 'newpage' => 'newpage', + 'minor' => 'minoredit', + 'bot' => 'botedit', + 'unpatrolled' => 'unpatrolled', + 'minoredit' => 'minoredit', + 'botedit' => 'botedit', + ); + $flag = $map[$flag]; + + return "<abbr class='$flag' title='" . $messages[$flag][1] . "'>" . $messages[$flag][0] . '</abbr>'; } /** @@ -141,12 +176,12 @@ class ChangesList { $this->rclistOpen = false; return ''; } - + /** * Show formatted char difference * @param $old Integer: bytes * @param $new Integer: bytes - * @returns String + * @return String */ public static function showCharacterDifference( $old, $new ) { global $wgRCChangedSizeThreshold, $wgLang, $wgMiserMode; @@ -157,29 +192,29 @@ class ChangesList { if ( !isset($fastCharDiff[$code]) ) { $fastCharDiff[$code] = $wgMiserMode || wfMsgNoTrans( 'rc-change-size' ) === '$1'; } - + $formatedSize = $wgLang->formatNum($szdiff); if ( !$fastCharDiff[$code] ) { $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape' ), $formatedSize ); } - + if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) { $tag = 'strong'; } else { - $tag = 'span'; + $tag = 'span'; } if( $szdiff === 0 ) { return "<$tag class='mw-plusminus-null'>($formatedSize)</$tag>"; } elseif( $szdiff > 0 ) { return "<$tag class='mw-plusminus-pos'>(+$formatedSize)</$tag>"; - } else { + } else { return "<$tag class='mw-plusminus-neg'>($formatedSize)</$tag>"; } } /** - * Returns text for the end of RC + * Returns text for the end of RC * @return String */ public function endRecentChangesList() { @@ -190,42 +225,38 @@ class ChangesList { } } + /** + * @param $s + * @param $rc RecentChange + * @return void + */ public function insertMove( &$s, $rc ) { # Diff $s .= '(' . $this->message['diff'] . ') ('; # Hist - $s .= $this->skin->link( + $s .= Linker::linkKnown( $rc->getMovedToTitle(), $this->message['hist'], array(), - array( 'action' => 'history' ), - array( 'known', 'noclasses' ) + array( 'action' => 'history' ) ) . ') . . '; # "[[x]] moved to [[y]]" $msg = ( $rc->mAttribs['rc_type'] == RC_MOVE ) ? '1movedto2' : '1movedto2_redir'; - $s .= wfMsg( + $s .= wfMsgHtml( $msg, - $this->skin->link( + Linker::linkKnown( $rc->getTitle(), null, array(), - array( 'redirect' => 'no' ), - array( 'known', 'noclasses' ) + array( 'redirect' => 'no' ) ), - $this->skin->link( - $rc->getMovedToTitle(), - null, - array(), - array(), - array( 'known', 'noclasses' ) - ) + Linker::linkKnown( $rc->getMovedToTitle() ) ); } public function insertDateHeader( &$s, $rc_timestamp ) { - global $wgLang; # Make date header if necessary - $date = $wgLang->date( $rc_timestamp, true, true ); + $date = $this->getLang()->date( $rc_timestamp, true, true ); if( $date != $this->lastdate ) { if( $this->lastdate != '' ) { $s .= "</ul>\n"; @@ -238,20 +269,20 @@ class ChangesList { public function insertLog( &$s, $title, $logtype ) { $logname = LogPage::logName( $logtype ); - $s .= '(' . $this->skin->link( - $title, - $logname, - array(), - array(), - array( 'known', 'noclasses' ) - ) . ')'; + $s .= '(' . Linker::linkKnown( $title, htmlspecialchars( $logname ) ) . ')'; } + /** + * @param $s + * @param $rc RecentChange + * @param $unpatrolled + * @return void + */ public function insertDiffHist( &$s, &$rc, $unpatrolled ) { # Diff link if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { $diffLink = $this->message['diff']; - } else if( !self::userCan($rc,Revision::DELETED_TEXT) ) { + } elseif( !self::userCan($rc,Revision::DELETED_TEXT) ) { $diffLink = $this->message['diff']; } else { $query = array( @@ -264,31 +295,35 @@ class ChangesList { $query['rcid'] = $rc->mAttribs['rc_id']; }; - $diffLink = $this->skin->link( + $diffLink = Linker::linkKnown( $rc->getTitle(), $this->message['diff'], array( 'tabindex' => $rc->counter ), - $query, - array( 'known', 'noclasses' ) + $query ); } $s .= '(' . $diffLink . $this->message['pipe-separator']; # History link - $s .= $this->skin->link( + $s .= Linker::linkKnown( $rc->getTitle(), $this->message['hist'], array(), array( 'curid' => $rc->mAttribs['rc_cur_id'], 'action' => 'history' - ), - array( 'known', 'noclasses' ) + ) ); $s .= ') . . '; } + /** + * @param $s + * @param $rc RecentChange + * @param $unpatrolled + * @param $watched + * @return void + */ public function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) { - global $wgContLang; # If it's a new article, there is no diff link, but if it hasn't been # patrolled yet, we need to give users a way to do so $params = array(); @@ -298,21 +333,19 @@ class ChangesList { } if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) { - $articlelink = $this->skin->link( + $articlelink = Linker::linkKnown( $rc->getTitle(), null, array(), - $params, - array( 'known', 'noclasses' ) + $params ); $articlelink = '<span class="history-deleted">' . $articlelink . '</span>'; } else { - $articlelink = ' '. $this->skin->link( + $articlelink = ' '. Linker::linkKnown( $rc->getTitle(), null, array(), - $params, - array( 'known', 'noclasses' ) + $params ); } # Bolden pages watched by this user @@ -320,7 +353,7 @@ class ChangesList { $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>"; } # RTL/LTR marker - $articlelink .= $wgContLang->getDirMark(); + $articlelink .= $this->getLang()->getDirMark(); wfRunHooks( 'ChangesListInsertArticleLink', array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched) ); @@ -328,41 +361,54 @@ class ChangesList { $s .= " $articlelink"; } + /** + * @param $s + * @param $rc RecentChange + * @return void + */ public function insertTimestamp( &$s, $rc ) { - global $wgLang; - $s .= $this->message['semicolon-separator'] . - $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; + $s .= $this->message['semicolon-separator'] . + $this->getLang()->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; } - /** Insert links to user page, user talk page and eventually a blocking link */ + /** Insert links to user page, user talk page and eventually a blocking link + * + * @param $rc RecentChange + */ public function insertUserRelatedLinks( &$s, &$rc ) { if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) { - $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; + $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; } else { - $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); - $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + $s .= Linker::userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + $s .= Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); } } - /** insert a formatted action */ + /** insert a formatted action + * + * @param $rc RecentChange + */ public function insertAction( &$s, &$rc ) { if( $rc->mAttribs['rc_type'] == RC_LOG ) { if( $this->isDeleted( $rc, LogPage::DELETED_ACTION ) ) { $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>'; } else { $s .= ' '.LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'], - $rc->getTitle(), $this->skin, LogPage::extractParams( $rc->mAttribs['rc_params'] ), true, true ); + $rc->getTitle(), $this->getSkin(), LogPage::extractParams( $rc->mAttribs['rc_params'] ), true, true ); } } } - /** insert a formatted comment */ + /** insert a formatted comment + * + * @param $rc RecentChange + */ public function insertComment( &$s, &$rc ) { if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) { if( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) { $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>'; } else { - $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); + $s .= Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); } } } @@ -380,12 +426,11 @@ class ChangesList { * Returns the string which indicates the number of watching users */ protected function numberofWatchingusers( $count ) { - global $wgLang; static $cache = array(); if( $count > 0 ) { if( !isset( $cache[$count] ) ) { $cache[$count] = wfMsgExt( 'number_of_watching_users_RCview', - array('parsemag', 'escape' ), $wgLang->formatNum( $count ) ); + array('parsemag', 'escape' ), $this->getLang()->formatNum( $count ) ); } return $cache[$count]; } else { @@ -425,15 +470,18 @@ class ChangesList { return '<span class="mw-rc-unwatched">' . $link . '</span>'; } } - - /** Inserts a rollback link */ + + /** Inserts a rollback link + * + * @param $s + * @param $rc RecentChange + */ public function insertRollback( &$s, &$rc ) { - global $wgUser; if( !$rc->mAttribs['rc_new'] && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) { $page = $rc->getTitle(); /** Check for rollback and edit permissions, disallow special pages, and only * show a link on the top-most revision */ - if ($wgUser->isAllowed('rollback') && $rc->mAttribs['page_latest'] == $rc->mAttribs['rc_this_oldid'] ) + if ( $this->getUser()->isAllowed('rollback') && $rc->mAttribs['page_latest'] == $rc->mAttribs['rc_this_oldid'] ) { $rev = new Revision( array( 'id' => $rc->mAttribs['rc_this_oldid'], @@ -442,15 +490,21 @@ class ChangesList { 'deleted' => $rc->mAttribs['rc_deleted'] ) ); $rev->setTitle( $page ); - $s .= ' '.$this->skin->generateRollback( $rev ); + $s .= ' '.Linker::generateRollback( $rev, $this->getContext() ); } } } + /** + * @param $s + * @param $rc RecentChange + * @param $classes + * @return + */ public function insertTags( &$s, &$rc, &$classes ) { if ( empty($rc->mAttribs['ts_tags']) ) return; - + list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $rc->mAttribs['ts_tags'], 'changeslist' ); $classes = array_merge( $classes, $newClasses ); $s .= ' ' . $tagSummary; @@ -468,12 +522,14 @@ class ChangesList { class OldChangesList extends ChangesList { /** * Format a line using the old system (aka without any javascript). + * + * @param $rc RecentChange */ public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) { - global $wgLang, $wgRCShowChangedSize, $wgUser; + global $wgRCShowChangedSize; wfProfileIn( __METHOD__ ); # Should patrol-related stuff be shown? - $unpatrolled = $wgUser->useRCPatrol() && !$rc->mAttribs['rc_patrolled']; + $unpatrolled = $this->getUser()->useRCPatrol() && !$rc->mAttribs['rc_patrolled']; $dateheader = ''; // $s now contains only <li>...</li>, for hooks' convenience. $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] ); @@ -499,7 +555,7 @@ class OldChangesList extends ChangesList { $this->insertLog( $s, $logtitle, $rc->mAttribs['rc_log_type'] ); // Log entries (old format) or log targets, and special pages } elseif( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) { - list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $rc->mAttribs['rc_title'] ); + list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $rc->mAttribs['rc_title'] ); if( $name == 'Log' ) { $this->insertLog( $s, $rc->getTitle(), $subpage ); } @@ -507,8 +563,15 @@ class OldChangesList extends ChangesList { } else { $this->insertDiffHist( $s, $rc, $unpatrolled ); # M, N, b and ! (minor, new, bot and unpatrolled) - $s .= $this->recentChangesFlags( $rc->mAttribs['rc_new'], $rc->mAttribs['rc_minor'], - $unpatrolled, '', $rc->mAttribs['rc_bot'] ); + $s .= $this->recentChangesFlags( + array( + 'newpage' => $rc->mAttribs['rc_new'], + 'minor' => $rc->mAttribs['rc_minor'], + 'unpatrolled' => $unpatrolled, + 'bot' => $rc->mAttribs['rc_bot'] + ), + '' + ); $this->insertArticleLink( $s, $rc, $unpatrolled, $watched ); } # Edit/log timestamp @@ -522,6 +585,8 @@ class OldChangesList extends ChangesList { } # User tool links $this->insertUserRelatedLinks( $s, $rc ); + # LTR/RTL direction mark + $s .= $this->getLang()->getDirMark(); # Log action text (if any) $this->insertAction( $s, $rc ); # Edit or log comment @@ -532,17 +597,17 @@ class OldChangesList extends ChangesList { $this->insertRollback( $s, $rc ); # For subclasses $this->insertExtra( $s, $rc, $classes ); - + # How many users watch this page if( $rc->numberofWatchingusers > 0 ) { - $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview', - array( 'parsemag', 'escape' ), $wgLang->formatNum( $rc->numberofWatchingusers ) ); + $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview', + array( 'parsemag', 'escape' ), $this->getLang()->formatNum( $rc->numberofWatchingusers ) ); } - + if( $this->watchlist ) { $classes[] = Sanitizer::escapeClass( 'watchlist-'.$rc->mAttribs['rc_namespace'].'-'.$rc->mAttribs['rc_title'] ); } - + wfRunHooks( 'OldChangesListRecentChangesLine', array(&$this, &$s, $rc) ); wfProfileOut( __METHOD__ ); @@ -560,34 +625,32 @@ class EnhancedChangesList extends ChangesList { * @return String */ public function beginRecentChangesList() { - global $wgOut; $this->rc_cache = array(); $this->rcMoveIndex = 0; $this->rcCacheIndex = 0; $this->lastdate = ''; $this->rclistOpen = false; - $wgOut->addModules( 'mediawiki.legacy.enhancedchanges' ); + $this->getOutput()->addModuleStyles( 'mediawiki.special.changeslist' ); return ''; } /** * Format a line for enhanced recentchange (aka with javascript and block of lines). + * + * @param $baseRC RecentChange + * @param $watched bool + * + * @return string */ public function recentChangesLine( &$baseRC, $watched = false ) { - global $wgLang, $wgUser; - wfProfileIn( __METHOD__ ); # Create a specialised object $rc = RCCacheEntry::newFromParent( $baseRC ); - # Extract fields from DB into the function scope (rc_xxxx variables) - // FIXME: Would be good to replace this extract() call with something - // that explicitly initializes variables. - extract( $rc->mAttribs ); - $curIdEq = array( 'curid' => $rc_cur_id ); + $curIdEq = array( 'curid' => $rc->mAttribs['rc_cur_id'] ); # If it's a new day, add the headline and flush the cache - $date = $wgLang->date( $rc_timestamp, true ); + $date = $this->getLang()->date( $rc->mAttribs['rc_timestamp'], true ); $ret = ''; if( $date != $this->lastdate ) { # Process current cache @@ -598,48 +661,50 @@ class EnhancedChangesList extends ChangesList { } # Should patrol-related stuff be shown? - if( $wgUser->useRCPatrol() ) { - $rc->unpatrolled = !$rc_patrolled; + if( $this->getUser()->useRCPatrol() ) { + $rc->unpatrolled = !$rc->mAttribs['rc_patrolled']; } else { $rc->unpatrolled = false; } $showdifflinks = true; # Make article link + $type = $rc->mAttribs['rc_type']; + $logType = $rc->mAttribs['rc_log_type']; // Page moves - if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { - $msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; - $clink = wfMsg( $msg, $this->skin->linkKnown( $rc->getTitle(), null, + if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) { + $msg = ( $type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; + $clink = wfMsg( $msg, Linker::linkKnown( $rc->getTitle(), null, array(), array( 'redirect' => 'no' ) ), - $this->skin->linkKnown( $rc->getMovedToTitle() ) ); + Linker::linkKnown( $rc->getMovedToTitle() ) ); // New unpatrolled pages - } else if( $rc->unpatrolled && $rc_type == RC_NEW ) { - $clink = $this->skin->linkKnown( $rc->getTitle(), null, array(), - array( 'rcid' => $rc_id ) ); + } elseif( $rc->unpatrolled && $type == RC_NEW ) { + $clink = Linker::linkKnown( $rc->getTitle(), null, array(), + array( 'rcid' => $rc->mAttribs['rc_id'] ) ); // Log entries - } else if( $rc_type == RC_LOG ) { - if( $rc_log_type ) { - $logtitle = SpecialPage::getTitleFor( 'Log', $rc_log_type ); - $clink = '(' . $this->skin->linkKnown( $logtitle, - LogPage::logName($rc_log_type) ) . ')'; + } elseif( $type == RC_LOG ) { + if( $logType ) { + $logtitle = SpecialPage::getTitleFor( 'Log', $logType ); + $clink = '(' . Linker::linkKnown( $logtitle, + LogPage::logName( $logType ) ) . ')'; } else { - $clink = $this->skin->link( $rc->getTitle() ); + $clink = Linker::link( $rc->getTitle() ); } $watched = false; // Log entries (old format) and special pages - } elseif( $rc_namespace == NS_SPECIAL ) { - list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); + } elseif( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) { + list( $specialName, $logtype ) = SpecialPageFactory::resolveAlias( $rc->mAttribs['rc_title'] ); if ( $specialName == 'Log' ) { # Log updates, etc $logname = LogPage::logName( $logtype ); - $clink = '(' . $this->skin->linkKnown( $rc->getTitle(), $logname ) . ')'; + $clink = '(' . Linker::linkKnown( $rc->getTitle(), $logname ) . ')'; } else { wfDebug( "Unexpected special page in recentchanges\n" ); $clink = ''; } // Edits } else { - $clink = $this->skin->linkKnown( $rc->getTitle() ); + $clink = Linker::linkKnown( $rc->getTitle() ); } # Don't show unusable diff links @@ -647,7 +712,7 @@ class EnhancedChangesList extends ChangesList { $showdifflinks = false; } - $time = $wgLang->time( $rc_timestamp, true, true ); + $time = $this->getLang()->time( $rc->mAttribs['rc_timestamp'], true, true ); $rc->watched = $watched; $rc->link = $clink; $rc->timestamp = $time; @@ -655,20 +720,22 @@ class EnhancedChangesList extends ChangesList { # Make "cur" and "diff" links. Do not use link(), it is too slow if # called too many times (50% of CPU time on RecentChanges!). + $thisOldid = $rc->mAttribs['rc_this_oldid']; + $lastOldid = $rc->mAttribs['rc_last_oldid']; if( $rc->unpatrolled ) { - $rcIdQuery = array( 'rcid' => $rc_id ); + $rcIdQuery = array( 'rcid' => $rc->mAttribs['rc_id'] ); } else { $rcIdQuery = array(); } - $querycur = $curIdEq + array( 'diff' => '0', 'oldid' => $rc_this_oldid ); - $querydiff = $curIdEq + array( 'diff' => $rc_this_oldid, 'oldid' => - $rc_last_oldid ) + $rcIdQuery; + $querycur = $curIdEq + array( 'diff' => '0', 'oldid' => $thisOldid ); + $querydiff = $curIdEq + array( 'diff' => $thisOldid, 'oldid' => + $lastOldid ) + $rcIdQuery; if( !$showdifflinks ) { $curLink = $this->message['cur']; $diffLink = $this->message['diff']; - } else if( in_array( $rc_type, array(RC_NEW,RC_LOG,RC_MOVE,RC_MOVE_OVER_REDIRECT) ) ) { - if ( $rc_type != RC_NEW ) { + } elseif( in_array( $type, array( RC_NEW, RC_LOG, RC_MOVE, RC_MOVE_OVER_REDIRECT ) ) ) { + if ( $type != RC_NEW ) { $curLink = $this->message['cur']; } else { $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) ); @@ -683,21 +750,21 @@ class EnhancedChangesList extends ChangesList { } # Make "last" link - if( !$showdifflinks || !$rc_last_oldid ) { + if( !$showdifflinks || !$lastOldid ) { $lastLink = $this->message['last']; - } else if( $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + } elseif( in_array( $type, array( RC_LOG, RC_MOVE, RC_MOVE_OVER_REDIRECT ) ) ) { $lastLink = $this->message['last']; } else { - $lastLink = $this->skin->linkKnown( $rc->getTitle(), $this->message['last'], - array(), $curIdEq + array('diff' => $rc_this_oldid, 'oldid' => $rc_last_oldid) + $rcIdQuery ); + $lastLink = Linker::linkKnown( $rc->getTitle(), $this->message['last'], + array(), $curIdEq + array('diff' => $thisOldid, 'oldid' => $lastOldid) + $rcIdQuery ); } # Make user links - if( $this->isDeleted($rc,Revision::DELETED_USER) ) { - $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; + if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) { + $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; } else { - $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text ); - $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text ); + $rc->userlink = Linker::userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + $rc->usertalklink = Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); } $rc->lastlink = $lastLink; @@ -708,13 +775,13 @@ class EnhancedChangesList extends ChangesList { # Page moves go on their own line $title = $rc->getTitle(); $secureName = $title->getPrefixedDBkey(); - if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) { # Use an @ character to prevent collision with page names $this->rc_cache['@@' . ($this->rcMoveIndex++)] = array($rc); } else { # Logs are grouped by type - if( $rc_type == RC_LOG ){ - $secureName = SpecialPage::getTitleFor( 'Log', $rc_log_type )->getPrefixedDBkey(); + if( $type == RC_LOG ){ + $secureName = SpecialPage::getTitleFor( 'Log', $logType )->getPrefixedDBkey(); } if( !isset( $this->rc_cache[$secureName] ) ) { $this->rc_cache[$secureName] = array(); @@ -732,16 +799,16 @@ class EnhancedChangesList extends ChangesList { * Enhanced RC group */ protected function recentChangesBlockGroup( $block ) { - global $wgLang, $wgContLang, $wgRCShowChangedSize; + global $wgRCShowChangedSize; wfProfileIn( __METHOD__ ); # Add the namespace and title of the block as part of the class if ( $block[0]->mAttribs['rc_log_type'] ) { # Log entry - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $block[0]->mAttribs['rc_log_type'] . '-' . $block[0]->mAttribs['rc_title'] ); + $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $block[0]->mAttribs['rc_log_type'] . '-' . $block[0]->mAttribs['rc_title'] ); } else { - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] ); + $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] ); } $r = Html::openElement( 'table', array( 'class' => $classes ) ) . Html::openElement( 'tr' ); @@ -794,51 +861,56 @@ class EnhancedChangesList extends ChangesList { $users = array(); foreach( $userlinks as $userlink => $count) { $text = $userlink; - $text .= $wgContLang->getDirMark(); + $text .= $this->getLang()->getDirMark(); if( $count > 1 ) { - $text .= ' (' . $wgLang->formatNum( $count ) . '×)'; + $text .= ' (' . $this->getLang()->formatNum( $count ) . '×)'; } array_push( $users, $text ); } - $users = ' <span class="changedby">[' . + $users = ' <span class="changedby">[' . implode( $this->message['semicolon-separator'], $users ) . ']</span>'; - # ID for JS visibility toggle - $jsid = $this->rcCacheIndex; - # onclick handler to toggle hidden/expanded - $toggleLink = "onclick='toggleVisibility($jsid); return false'"; # Title for <a> tags $expandTitle = htmlspecialchars( wfMsg( 'rc-enhanced-expand' ) ); $closeTitle = htmlspecialchars( wfMsg( 'rc-enhanced-hide' ) ); - $tl = "<span id='mw-rc-openarrow-$jsid' class='mw-changeslist-expanded' style='visibility:hidden'><a href='#' $toggleLink title='$expandTitle'>" . $this->sideArrow() . "</a></span>"; - $tl .= "<span id='mw-rc-closearrow-$jsid' class='mw-changeslist-hidden' style='display:none'><a href='#' $toggleLink title='$closeTitle'>" . $this->downArrow() . "</a></span>"; - $r .= '<td class="mw-enhanced-rc">'.$tl.' '; + $tl = "<span class='mw-collapsible-toggle'>" + . "<span class='mw-rc-openarrow'>" + . "<a href='#' title='$expandTitle'>{$this->sideArrow()}</a>" + . "</span><span class='mw-rc-closearrow'>" + . "<a href='#' title='$closeTitle'>{$this->downArrow()}</a>" + . "</span></span>"; + $r .= "<td>$tl</td>"; # Main line - $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, ' ', $bot ); + $r .= '<td class="mw-enhanced-rc">' . $this->recentChangesFlags( array( + 'newpage' => $isnew, + 'minor' => false, + 'unpatrolled' => $unpatrolled, + 'bot' => $bot , + ) ); # Timestamp - $r .= ' '.$block[0]->timestamp.' </td><td style="padding:0px;">'; + $r .= ' '.$block[0]->timestamp.' </td><td>'; # Article link if( $namehidden ) { $r .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>'; - } else if( $allLogs ) { + } elseif( $allLogs ) { $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); } else { $this->insertArticleLink( $r, $block[0], $block[0]->unpatrolled, $block[0]->watched ); } - $r .= $wgContLang->getDirMark(); + $r .= $this->getLang()->getDirMark(); $queryParams['curid'] = $curId; # Changes message $n = count($block); static $nchanges = array(); if ( !isset( $nchanges[$n] ) ) { - $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $wgLang->formatNum( $n ) ); + $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $this->getLang()->formatNum( $n ) ); } # Total change link $r .= ' '; @@ -846,14 +918,14 @@ class EnhancedChangesList extends ChangesList { $r .= '('; if( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT ) ) { $r .= $nchanges[$n]; - } else if( $isnew ) { + } elseif( $isnew ) { $r .= $nchanges[$n]; } else { $params = $queryParams; $params['diff'] = $currentRevision; $params['oldid'] = $oldid; - - $r .= $this->skin->link( + + $r .= Linker::link( $block[0]->getTitle(), $nchanges[$n], array(), @@ -866,19 +938,18 @@ class EnhancedChangesList extends ChangesList { # History if( $allLogs ) { // don't show history link for logs - } else if( $namehidden || !$block[0]->getTitle()->exists() ) { + } elseif( $namehidden || !$block[0]->getTitle()->exists() ) { $r .= $this->message['pipe-separator'] . $this->message['hist'] . ')'; } else { $params = $queryParams; $params['action'] = 'history'; $r .= $this->message['pipe-separator'] . - $this->skin->link( + Linker::linkKnown( $block[0]->getTitle(), $this->message['hist'], array(), - $params, - array( 'known', 'noclasses' ) + $params ) . ')'; } $r .= ' . . '; @@ -908,55 +979,51 @@ class EnhancedChangesList extends ChangesList { $r .= $users; $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers); - $r .= "</td></tr></table>\n"; - # Sub-entries - $r .= '<div id="mw-rc-subentries-'.$jsid.'" class="mw-changeslist-hidden">'; - $r .= '<table class="mw-enhanced-rc">'; foreach( $block as $rcObj ) { - # Extract fields from DB into the function scope (rc_xxxx variables) - // FIXME: Would be good to replace this extract() call with something - // that explicitly initializes variables. # Classes to apply -- TODO implement $classes = array(); - extract( $rcObj->mAttribs ); + $type = $rcObj->mAttribs['rc_type']; #$r .= '<tr><td valign="top">'.$this->spacerArrow(); - $r .= '<tr><td style="vertical-align:top;font-family:monospace; padding:0px;">'; - $r .= $this->spacerIndent() . $this->spacerIndent(); - $r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); - $r .= ' </td><td style="vertical-align:top; padding:0px;"><span style="font-family:monospace">'; + $r .= '<tr><td></td><td class="mw-enhanced-rc">'; + $r .= $this->recentChangesFlags( array( + 'newpage' => $rcObj->mAttribs['rc_new'], + 'minor' => $rcObj->mAttribs['rc_minor'], + 'unpatrolled' => $rcObj->unpatrolled, + 'bot' => $rcObj->mAttribs['rc_bot'], + ) ); + $r .= ' </td><td class="mw-enhanced-rc-nested"><span class="mw-enhanced-rc-time">'; $params = $queryParams; - if( $rc_this_oldid != 0 ) { - $params['oldid'] = $rc_this_oldid; + if( $rcObj->mAttribs['rc_this_oldid'] != 0 ) { + $params['oldid'] = $rcObj->mAttribs['rc_this_oldid']; } # Log timestamp - if( $rc_type == RC_LOG ) { + if( $type == RC_LOG ) { $link = $rcObj->timestamp; # Revision link - } else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { + } elseif( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { $link = '<span class="history-deleted">'.$rcObj->timestamp.'</span> '; } else { - if ( $rcObj->unpatrolled && $rc_type == RC_NEW) { + if ( $rcObj->unpatrolled && $type == RC_NEW) { $params['rcid'] = $rcObj->mAttribs['rc_id']; } - $link = $this->skin->link( + $link = Linker::linkKnown( $rcObj->getTitle(), $rcObj->timestamp, array(), - $params, - array( 'known', 'noclasses' ) + $params ); if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) $link = '<span class="history-deleted">'.$link.'</span> '; } $r .= $link . '</span>'; - if ( !$rc_type == RC_LOG || $rc_type == RC_NEW ) { + if ( !$type == RC_LOG || $type == RC_NEW ) { $r .= ' ('; $r .= $rcObj->curlink; $r .= $this->message['pipe-separator']; @@ -966,9 +1033,10 @@ class EnhancedChangesList extends ChangesList { $r .= ' . . '; # Character diff - if( $wgRCShowChangedSize ) { - $r .= ( $rcObj->getCharacterDifference() == '' ? '' : $rcObj->getCharacterDifference() . ' . . ' ) ; + if( $wgRCShowChangedSize && $rcObj->getCharacterDifference() ) { + $r .= $rcObj->getCharacterDifference() . ' . . ' ; } + # User links $r .= $rcObj->userlink; $r .= $rcObj->usertalklink; @@ -983,7 +1051,7 @@ class EnhancedChangesList extends ChangesList { $r .= "</td></tr>\n"; } - $r .= "</table></div>\n"; + $r .= "</table>\n"; $this->rcCacheIndex++; @@ -1013,8 +1081,8 @@ class EnhancedChangesList extends ChangesList { * @return String: HTML <img> tag */ protected function sideArrow() { - global $wgContLang; - $dir = $wgContLang->isRTL() ? 'l' : 'r'; + global $wgLang; + $dir = $wgLang->isRTL() ? 'l' : 'r'; return $this->arrow( $dir, '+', wfMsg( 'rc-enhanced-expand' ) ); } @@ -1036,70 +1104,58 @@ class EnhancedChangesList extends ChangesList { } /** - * Add a set of spaces - * @return String: HTML <td> tag - */ - protected function spacerIndent() { - return '     '; - } - - /** * Enhanced RC ungrouped line. + * + * @param $rcObj RecentChange * @return String: a HTML formated line (generated using $r) */ protected function recentChangesBlockLine( $rcObj ) { global $wgRCShowChangedSize; wfProfileIn( __METHOD__ ); + $query['curid'] = $rcObj->mAttribs['rc_cur_id']; - # Extract fields from DB into the function scope (rc_xxxx variables) - // FIXME: Would be good to replace this extract() call with something - // that explicitly initializes variables. - // TODO implement - extract( $rcObj->mAttribs ); - $query['curid'] = $rc_cur_id; - - if( $rc_log_type ) { + $type = $rcObj->mAttribs['rc_type']; + $logType = $rcObj->mAttribs['rc_log_type']; + if( $logType ) { # Log entry - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $rc_log_type . '-' . $rcObj->mAttribs['rc_title'] ); + $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType . '-' . $rcObj->mAttribs['rc_title'] ); } else { $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . $rcObj->mAttribs['rc_namespace'] . '-' . $rcObj->mAttribs['rc_title'] ); } $r = Html::openElement( 'table', array( 'class' => $classes ) ) . Html::openElement( 'tr' ); - $r .= '<td class="mw-enhanced-rc">' . $this->spacerArrow() . ' '; + $r .= '<td class="mw-enhanced-rc">' . $this->spacerArrow(); # Flag and Timestamp - if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) { $r .= '    '; // 4 flags -> 4 spaces } else { - $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); + $r .= $this->recentChangesFlags( array( + 'newpage' => $type == RC_NEW, + 'minor' => $rcObj->mAttribs['rc_minor'], + 'unpatrolled' => $rcObj->unpatrolled, + 'bot' => $rcObj->mAttribs['rc_bot'], + ) ); } - $r .= ' '.$rcObj->timestamp.' </td><td style="padding:0px;">'; + $r .= ' '.$rcObj->timestamp.' </td><td>'; # Article or log link - if( $rc_log_type ) { - $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL ); - $logname = LogPage::logName( $rc_log_type ); - $r .= '(' . $this->skin->link( - $logtitle, - $logname, - array(), - array(), - array( 'known', 'noclasses' ) - ) . ')'; + if( $logType ) { + $logtitle = SpecialPage::getTitleFor( 'Log', $logType ); + $logname = LogPage::logName( $logType ); + $r .= '(' . Linker::linkKnown( $logtitle, htmlspecialchars( $logname ) ) . ')'; } else { $this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched ); } # Diff and hist links - if ( $rc_type != RC_LOG ) { + if ( $type != RC_LOG ) { $r .= ' ('. $rcObj->difflink . $this->message['pipe-separator']; $query['action'] = 'history'; - $r .= $this->skin->link( + $r .= Linker::linkKnown( $rcObj->getTitle(), $this->message['hist'], array(), - $query, - array( 'known', 'noclasses' ) + $query ) . ')'; } $r .= ' . . '; @@ -1110,12 +1166,12 @@ class EnhancedChangesList extends ChangesList { # User/talk $r .= ' '.$rcObj->userlink . $rcObj->usertalklink; # Log action (if any) - if( $rc_log_type ) { + if( $logType ) { if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) { $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; } else { - $r .= ' ' . LogPage::actionText( $rc_log_type, $rc_log_action, $rcObj->getTitle(), - $this->skin, LogPage::extractParams($rc_params), true, true ); + $r .= ' ' . LogPage::actionText( $logType, $rcObj->mAttribs['rc_log_action'], $rcObj->getTitle(), + $this->getSkin(), LogPage::extractParams( $rcObj->mAttribs['rc_params'] ), true, true ); } } $this->insertComment( $r, $rcObj ); @@ -1136,6 +1192,8 @@ class EnhancedChangesList extends ChangesList { /** * If enhanced RC is in use, this function takes the previously cached * RC lines, arranges them, and outputs the HTML + * + * @return string */ protected function recentChangesBlock() { if( count ( $this->rc_cache ) == 0 ) { @@ -1159,7 +1217,7 @@ class EnhancedChangesList extends ChangesList { } /** - * Returns text for the end of RC + * Returns text for the end of RC * If enhanced RC is in use, returns pretty much all the text */ public function endRecentChangesList() { diff --git a/includes/Collation.php b/includes/Collation.php index f00b568f..0c510b78 100644 --- a/includes/Collation.php +++ b/includes/Collation.php @@ -3,6 +3,9 @@ abstract class Collation { static $instance; + /** + * @return Collation + */ static function singleton() { if ( !self::$instance ) { global $wgCategoryCollation; @@ -11,13 +14,30 @@ abstract class Collation { return self::$instance; } + /** + * @throws MWException + * @param $collationName string + * @return Collation + */ static function factory( $collationName ) { switch( $collationName ) { case 'uppercase': return new UppercaseCollation; + case 'identity': + return new IdentityCollation; case 'uca-default': return new IcuCollation( 'root' ); default: + # Provide a mechanism for extensions to hook in. + + $collationObject = null; + wfRunHooks( 'Collation::factory', array( $collationName, &$collationObject ) ); + + if ( $collationObject instanceof Collation ) { + return $collationObject; + } + + // If all else fails... throw new MWException( __METHOD__.": unknown collation type \"$collationName\"" ); } } @@ -81,6 +101,30 @@ class UppercaseCollation extends Collation { } } +/** + * Collation class that's essentially a no-op. + * + * Does sorting based on binary value of the string. + * Like how things were pre 1.17. + */ +class IdentityCollation extends Collation { + + function getSortKey( $string ) { + return $string; + } + + function getFirstLetter( $string ) { + global $wgContLang; + // Copied from UppercaseCollation. + // I'm kind of unclear on when this could happen... + if ( $string[0] == "\0" ) { + $string = substr( $string, 1 ); + } + return $wgContLang->firstChar( $string ); + } +} + + class IcuCollation extends Collation { var $primaryCollator, $mainCollator, $locale; var $firstLetterData; @@ -259,13 +303,13 @@ class IcuCollation extends Collation { * Do a binary search, and return the index of the largest item that sorts * less than or equal to the target value. * - * @param $valueCallback A function to call to get the value with + * @param $valueCallback array A function to call to get the value with * a given array index. - * @param $valueCount The number of items accessible via $valueCallback, + * @param $valueCount int The number of items accessible via $valueCallback, * indexed from 0 to $valueCount - 1 - * @param $comparisonCallback A callback to compare two values, returning + * @param $comparisonCallback array A callback to compare two values, returning * -1, 0 or 1 in the style of strcmp(). - * @param $target The target value to find. + * @param $target string The target value to find. * * @return The item index of the lower bound, or false if the target value * sorts before all items. diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php index b08b77df..42a7173d 100644 --- a/includes/ConfEditor.php +++ b/includes/ConfEditor.php @@ -1,6 +1,6 @@ <?php -/** +/** * This is a state machine style parser with two internal stacks: * * A next state stack, which determines the state the machine will progress to next * * A path stack, which keeps track of the logical location in the file. @@ -40,8 +40,8 @@ class ConfEditor { /** The previous ConfEditorToken object */ var $prevToken; - /** - * The state machine stack. This is an array of strings where the topmost + /** + * The state machine stack. This is an array of strings where the topmost * element will be popped off and become the next parser state. */ var $stateStack; @@ -66,7 +66,7 @@ class ConfEditor { var $pathStack; /** - * The elements of the top of the pathStack for every path encountered, indexed + * The elements of the top of the pathStack for every path encountered, indexed * by slash-separated path. */ var $pathInfo; @@ -77,13 +77,17 @@ class ConfEditor { var $serial; /** - * Editor state. This consists of the internal copy/insert operations which + * Editor state. This consists of the internal copy/insert operations which * are applied to the source string to obtain the destination string. */ var $edits; /** * Simple entry point for command-line testing + * + * @param $text string + * + * @return string */ static function test( $text ) { try { @@ -103,7 +107,7 @@ class ConfEditor { } /** - * Edit the text. Returns the edited text. + * Edit the text. Returns the edited text. * @param $ops Array of operations. * * Operations are given as an associative array, with members: @@ -114,20 +118,20 @@ class ConfEditor { * * delete * Deletes an array element or statement with the specified path. - * e.g. + * e.g. * array('type' => 'delete', 'path' => '$foo/bar/baz' ) * is equivalent to the runtime PHP code: * unset( $foo['bar']['baz'] ); * * set - * Sets the value of an array element. If the element doesn't exist, it - * is appended to the array. If it does exist, the value is set, with + * Sets the value of an array element. If the element doesn't exist, it + * is appended to the array. If it does exist, the value is set, with * comments and indenting preserved. * * append * Appends a new element to the end of the array. Adds a trailing comma. * e.g. - * array( 'type' => 'append', 'path', '$foo/bar', + * array( 'type' => 'append', 'path', '$foo/bar', * 'key' => 'baz', 'value' => "'x'" ) * is like the PHP code: * $foo['bar']['baz'] = 'x'; @@ -187,7 +191,7 @@ class ConfEditor { list( $indent, ) = $this->getIndent( $start ); $textToInsert = "$indent$value,"; } else { - list( $indent, $arrowIndent ) = + list( $indent, $arrowIndent ) = $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] ); $textToInsert = "$indent$key$arrowIndent=> $value,"; } @@ -210,7 +214,7 @@ class ConfEditor { list( $indent, ) = $this->getIndent( $start ); $textToInsert = "$indent$value,"; } else { - list( $indent, $arrowIndent ) = + list( $indent, $arrowIndent ) = $this->getIndent( $start, $key, $info['arrowByte'] ); $textToInsert = "$indent$key$arrowIndent=> $value,"; } @@ -239,13 +243,13 @@ class ConfEditor { try { $this->parse(); } catch ( ConfEditorParseError $e ) { - throw new MWException( + throw new MWException( "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " . $e->getMessage() ); } return $out; } - + /** * Get the variables defined in the text * @return array( varname => value ) @@ -266,7 +270,7 @@ class ConfEditor { strlen( $trimmedPath ) - strlen( $name ) ); if( substr( $parentPath, -1 ) == '/' ) $parentPath = substr( $parentPath, 0, -1 ); - + $value = substr( $this->text, $data['valueStartByte'], $data['valueEndByte'] - $data['valueStartByte'] ); @@ -275,7 +279,7 @@ class ConfEditor { } return $vars; } - + /** * Set a value in an array, unless it's set already. For instance, * setVar( $arr, 'foo/bar', 'baz', 3 ); will set @@ -298,7 +302,7 @@ class ConfEditor { if ( !isset( $target[$key] ) ) $target[$key] = $value; } - + /** * Parse a scalar value in PHP * @return mixed Parsed value @@ -306,14 +310,14 @@ class ConfEditor { function parseScalar( $str ) { if ( $str !== '' && $str[0] == '\'' ) // Single-quoted string - // @todo Fixme: trim() call is due to mystery bug where whitespace gets + // @todo FIXME: trim() call is due to mystery bug where whitespace gets // appended to the token; without it we ended up reading in the // extra quote on the end! return strtr( substr( trim( $str ), 1, -1 ), array( '\\\'' => '\'', '\\\\' => '\\' ) ); - if ( $str !== '' && @$str[0] == '"' ) + if ( $str !== '' && $str[0] == '"' ) // Double-quoted string - // @todo Fixme: trim() call is due to mystery bug where whitespace gets + // @todo FIXME: trim() call is due to mystery bug where whitespace gets // appended to the token; without it we ended up reading in the // extra quote on the end! return stripcslashes( substr( trim( $str ), 1, -1 ) ); @@ -364,8 +368,8 @@ class ConfEditor { } /** - * Finds the source byte region which you would want to delete, if $pathName - * was to be deleted. Includes the leading spaces and tabs, the trailing line + * Finds the source byte region which you would want to delete, if $pathName + * was to be deleted. Includes the leading spaces and tabs, the trailing line * break, and any comments in between. */ function findDeletionRegion( $pathName ) { @@ -419,9 +423,9 @@ class ConfEditor { } /** - * Find the byte region in the source corresponding to the value part. - * This includes the quotes, but does not include the trailing comma - * or semicolon. + * Find the byte region in the source corresponding to the value part. + * This includes the quotes, but does not include the trailing comma + * or semicolon. * * The end position is the past-the-end (end + 1) value as per convention. */ @@ -472,7 +476,7 @@ class ConfEditor { return $extraPath; } - /* + /** * Find the path name of first element in the array. * If the array is empty, this will return the \@extra interstitial element. * If the specified path is not found or is not an array, it will return false. @@ -519,7 +523,7 @@ class ConfEditor { } /** - * Run the parser on the text. Throws an exception if the string does not + * Run the parser on the text. Throws an exception if the string does not * match our defined subset of PHP syntax. */ public function parse() { @@ -706,7 +710,7 @@ class ConfEditor { } /** - * Set the parse position. Do not call this except from firstToken() and + * Set the parse position. Do not call this except from firstToken() and * nextToken(), there is more to update than just the position. */ protected function setPos( $pos ) { @@ -800,7 +804,7 @@ class ConfEditor { if ( $this->currentToken && $this->currentToken->type == $type ) { return $this->nextToken(); } else { - $this->error( "expected " . $this->getTypeName( $type ) . + $this->error( "expected " . $this->getTypeName( $type ) . ", got " . $this->getTypeName( $this->currentToken->type ) ); } } @@ -875,7 +879,7 @@ class ConfEditor { } /** - * Go to the next path on the same level. This ends the current path and + * Go to the next path on the same level. This ends the current path and * starts a new one. If $path is \@next, the new path is set to the next * numeric array element. */ @@ -889,7 +893,7 @@ class ConfEditor { } else { $this->pathStack[$i]['name'] = $path; } - $this->pathStack[$i] = + $this->pathStack[$i] = array( 'startByte' => $this->byteNum, 'startToken' => $this->pos, @@ -955,8 +959,8 @@ class ConfEditor { } /** - * Looks ahead to see if the given type is the next token type, starting - * from the current position plus the given offset. Skips any intervening + * Looks ahead to see if the given type is the next token type, starting + * from the current position plus the given offset. Skips any intervening * whitespace. */ function isAhead( $type, $offset = 0 ) { @@ -992,8 +996,8 @@ class ConfEditor { $out = ''; foreach ( $this->tokens as $token ) { $obj = $this->newTokenObj( $token ); - $out .= sprintf( "%-28s %s\n", - $this->getTypeName( $obj->type ), + $out .= sprintf( "%-28s %s\n", + $this->getTypeName( $obj->type ), addcslashes( $obj->text, "\0..\37" ) ); } echo "<pre>" . htmlspecialchars( $out ) . "</pre>"; @@ -1008,7 +1012,7 @@ class ConfEditorParseError extends MWException { function __construct( $editor, $msg ) { $this->lineNum = $editor->lineNum; $this->colNum = $editor->colNum; - parent::__construct( "Parse error on line {$editor->lineNum} " . + parent::__construct( "Parse error on line {$editor->lineNum} " . "col {$editor->colNum}: $msg" ); } @@ -1028,7 +1032,7 @@ class ConfEditorParseError extends MWException { */ class ConfEditorToken { var $type, $text; - + static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING ); static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ); diff --git a/includes/Cookie.php b/includes/Cookie.php new file mode 100644 index 00000000..95a4599f --- /dev/null +++ b/includes/Cookie.php @@ -0,0 +1,245 @@ +<?php +/** + * @defgroup HTTP HTTP + */ + +class Cookie { + protected $name; + protected $value; + protected $expires; + protected $path; + protected $domain; + protected $isSessionKey = true; + // TO IMPLEMENT protected $secure + // TO IMPLEMENT? protected $maxAge (add onto expires) + // TO IMPLEMENT? protected $version + // TO IMPLEMENT? protected $comment + + function __construct( $name, $value, $attr ) { + $this->name = $name; + $this->set( $value, $attr ); + } + + /** + * Sets a cookie. Used before a request to set up any individual + * cookies. Used internally after a request to parse the + * Set-Cookie headers. + * + * @param $value String: the value of the cookie + * @param $attr Array: possible key/values: + * expires A date string + * path The path this cookie is used on + * domain Domain this cookie is used on + */ + public function set( $value, $attr ) { + $this->value = $value; + + if ( isset( $attr['expires'] ) ) { + $this->isSessionKey = false; + $this->expires = strtotime( $attr['expires'] ); + } + + if ( isset( $attr['path'] ) ) { + $this->path = $attr['path']; + } else { + $this->path = '/'; + } + + if ( isset( $attr['domain'] ) ) { + if ( self::validateCookieDomain( $attr['domain'] ) ) { + $this->domain = $attr['domain']; + } + } else { + throw new MWException( 'You must specify a domain.' ); + } + } + + /** + * Return the true if the cookie is valid is valid. Otherwise, + * false. The uses a method similar to IE cookie security + * described here: + * http://kuza55.blogspot.com/2008/02/understanding-cookie-security.html + * A better method might be to use a blacklist like + * http://publicsuffix.org/ + * + * @fixme fails to detect 3-letter top-level domains + * @fixme fails to detect 2-letter top-level domains for single-domain use (probably not a big problem in practice, but there are test cases) + * + * @param $domain String: the domain to validate + * @param $originDomain String: (optional) the domain the cookie originates from + * @return Boolean + */ + public static function validateCookieDomain( $domain, $originDomain = null ) { + // Don't allow a trailing dot + if ( substr( $domain, -1 ) == '.' ) { + return false; + } + + $dc = explode( ".", $domain ); + + // Only allow full, valid IP addresses + if ( preg_match( '/^[0-9.]+$/', $domain ) ) { + if ( count( $dc ) != 4 ) { + return false; + } + + if ( ip2long( $domain ) === false ) { + return false; + } + + if ( $originDomain == null || $originDomain == $domain ) { + return true; + } + + } + + // Don't allow cookies for "co.uk" or "gov.uk", etc, but allow "supermarket.uk" + if ( strrpos( $domain, "." ) - strlen( $domain ) == -3 ) { + if ( ( count( $dc ) == 2 && strlen( $dc[0] ) <= 2 ) + || ( count( $dc ) == 3 && strlen( $dc[0] ) == "" && strlen( $dc[1] ) <= 2 ) ) { + return false; + } + if ( ( count( $dc ) == 2 || ( count( $dc ) == 3 && $dc[0] == '' ) ) + && preg_match( '/(com|net|org|gov|edu)\...$/', $domain ) ) { + return false; + } + } + + if ( $originDomain != null ) { + if ( substr( $domain, 0, 1 ) != '.' && $domain != $originDomain ) { + return false; + } + + if ( substr( $domain, 0, 1 ) == '.' + && substr_compare( $originDomain, $domain, -strlen( $domain ), + strlen( $domain ), true ) != 0 ) { + return false; + } + } + + return true; + } + + /** + * Serialize the cookie jar into a format useful for HTTP Request headers. + * + * @param $path String: the path that will be used. Required. + * @param $domain String: the domain that will be used. Required. + * @return String + */ + public function serializeToHttpRequest( $path, $domain ) { + $ret = ''; + + if ( $this->canServeDomain( $domain ) + && $this->canServePath( $path ) + && $this->isUnExpired() ) { + $ret = $this->name . '=' . $this->value; + } + + return $ret; + } + + protected function canServeDomain( $domain ) { + if ( $domain == $this->domain + || ( strlen( $domain ) > strlen( $this->domain ) + && substr( $this->domain, 0, 1 ) == '.' + && substr_compare( $domain, $this->domain, -strlen( $this->domain ), + strlen( $this->domain ), true ) == 0 ) ) { + return true; + } + + return false; + } + + protected function canServePath( $path ) { + if ( $this->path && substr_compare( $this->path, $path, 0, strlen( $this->path ) ) == 0 ) { + return true; + } + + return false; + } + + protected function isUnExpired() { + if ( $this->isSessionKey || $this->expires > time() ) { + return true; + } + + return false; + } +} + +class CookieJar { + private $cookie = array(); + + /** + * Set a cookie in the cookie jar. Make sure only one cookie per-name exists. + * @see Cookie::set() + */ + public function setCookie ( $name, $value, $attr ) { + /* cookies: case insensitive, so this should work. + * We'll still send the cookies back in the same case we got them, though. + */ + $index = strtoupper( $name ); + + if ( isset( $this->cookie[$index] ) ) { + $this->cookie[$index]->set( $value, $attr ); + } else { + $this->cookie[$index] = new Cookie( $name, $value, $attr ); + } + } + + /** + * @see Cookie::serializeToHttpRequest + */ + public function serializeToHttpRequest( $path, $domain ) { + $cookies = array(); + + foreach ( $this->cookie as $c ) { + $serialized = $c->serializeToHttpRequest( $path, $domain ); + + if ( $serialized ) { + $cookies[] = $serialized; + } + } + + return implode( '; ', $cookies ); + } + + /** + * Parse the content of an Set-Cookie HTTP Response header. + * + * @param $cookie String + * @param $domain String: cookie's domain + */ + public function parseCookieResponseHeader ( $cookie, $domain ) { + $len = strlen( 'Set-Cookie:' ); + + if ( substr_compare( 'Set-Cookie:', $cookie, 0, $len, true ) === 0 ) { + $cookie = substr( $cookie, $len ); + } + + $bit = array_map( 'trim', explode( ';', $cookie ) ); + + if ( count( $bit ) >= 1 ) { + list( $name, $value ) = explode( '=', array_shift( $bit ), 2 ); + $attr = array(); + + foreach ( $bit as $piece ) { + $parts = explode( '=', $piece ); + if ( count( $parts ) > 1 ) { + $attr[strtolower( $parts[0] )] = $parts[1]; + } else { + $attr[strtolower( $parts[0] )] = true; + } + } + + if ( !isset( $attr['domain'] ) ) { + $attr['domain'] = $domain; + } elseif ( !Cookie::validateCookieDomain( $attr['domain'], $domain ) ) { + return null; + } + + $this->setCookie( $name, $value, $attr ); + } + } +} diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 0395633d..4248add7 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -26,58 +26,43 @@ if( !defined( 'MEDIAWIKI' ) ) { die( 1 ); } -# Create a site configuration object. Not used for much in a default install -if ( !defined( 'MW_PHP4' ) ) { - require_once( "$IP/includes/SiteConfiguration.php" ); - $wgConf = new SiteConfiguration; -} +# Create a site configuration object. Not used for much in a default install. +# Note: this (and other things) will break if the autoloader is not enabled. +# Please include includes/AutoLoader.php before including this file. +$wgConf = new SiteConfiguration; /** @endcond */ /** MediaWiki version number */ -$wgVersion = '1.17.1'; +$wgVersion = '1.18.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; /** - * URL of the server. It will be automatically built including https mode. + * URL of the server. * * Example: * <code> - * $wgServer = http://example.com + * $wgServer = 'http://example.com'; * </code> * * This is usually detected correctly by MediaWiki. If MediaWiki detects the * wrong server, it will redirect incorrectly after you save a page. In that * case, set this variable to fix it. + * + * If you want to use protocol-relative URLs on your wiki, set this to a + * protocol-relative URL like '//example.com' and set $wgCanonicalServer + * to a fully qualified URL. */ -$wgServer = ''; - -/** @cond file_level_code */ -if( isset( $_SERVER['SERVER_NAME'] ) ) { - $serverName = $_SERVER['SERVER_NAME']; -} elseif( isset( $_SERVER['HOSTNAME'] ) ) { - $serverName = $_SERVER['HOSTNAME']; -} elseif( isset( $_SERVER['HTTP_HOST'] ) ) { - $serverName = $_SERVER['HTTP_HOST']; -} elseif( isset( $_SERVER['SERVER_ADDR'] ) ) { - $serverName = $_SERVER['SERVER_ADDR']; -} else { - $serverName = 'localhost'; -} - -$wgProto = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http'; +$wgServer = WebRequest::detectServer(); -$wgServer = $wgProto.'://' . $serverName; -# If the port is a non-standard one, add it to the URL -if( isset( $_SERVER['SERVER_PORT'] ) - && !strpos( $serverName, ':' ) - && ( ( $wgProto == 'http' && $_SERVER['SERVER_PORT'] != 80 ) - || ( $wgProto == 'https' && $_SERVER['SERVER_PORT'] != 443 ) ) ) { - - $wgServer .= ":" . $_SERVER['SERVER_PORT']; -} -/** @endcond */ +/** + * Canonical URL of the server, to use in IRC feeds and notification e-mails. + * Must be fully qualified, even if $wgServer is protocol-relative. + * + * Defaults to $wgServer, expanded to a fully qualified http:// URL if needed. + */ +$wgCanonicalServer = false; /************************************************************************//** * @name Script path settings @@ -147,6 +132,7 @@ $wgRedirectScript = false; ///< defaults to */ $wgLoadScript = false; + /**@}*/ /************************************************************************//** @@ -183,6 +169,7 @@ $wgLocalStylePath = false; /** * The URL path of the extensions directory. * Defaults to "{$wgScriptPath}/extensions". + * @since 1.16 */ $wgExtensionAssetsPath = false; @@ -228,23 +215,6 @@ $wgFavicon = '/favicon.ico'; $wgAppleTouchIcon = false; /** - * The URL path of the math directory. Defaults to "{$wgUploadPath}/math". - * - * See http://www.mediawiki.org/wiki/Manual:Enable_TeX for details about how to - * set up mathematical formula display. - */ -$wgMathPath = false; - -/** - * The filesystem path of the math directory. - * Defaults to "{$wgUploadDirectory}/math". - * - * See http://www.mediawiki.org/wiki/Manual:Enable_TeX for details about how to - * set up mathematical formula display. - */ -$wgMathDirectory = false; - -/** * The local filesystem path to a temporary directory. This is not required to * be web accessible. * @@ -295,7 +265,7 @@ $wgAllowImageMoving = true; $wgIllegalFileChars = ":"; /** - * @deprecated use $wgDeletedDirectory + * @deprecated since 1.17 use $wgDeletedDirectory */ $wgFileStore = array(); @@ -327,7 +297,7 @@ $wgImgAuthPublicTest = true; * - class The class name for the repository. May come from the core or an extension. * The core repository classes are LocalRepo, ForeignDBRepo, FSRepo. * - * - name A unique name for the repository. + * - name A unique name for the repository (but $wgLocalFileRepo should be 'local'). * * For most core repos: * - url Base public URL @@ -373,8 +343,12 @@ $wgImgAuthPublicTest = true; * - apibase Use for the foreign API's URL * - apiThumbCacheExpiry How long to locally cache thumbs for * - * The default is to initialise these arrays from the MW<1.11 backwards compatible settings: - * $wgUploadPath, $wgThumbnailScriptPath, $wgSharedUploadDirectory, etc. + * If you leave $wgLocalFileRepo set to false, Setup will fill in appropriate values. + * Otherwise, set $wgLocalFileRepo to a repository structure as described above. + * If you set $wgUseInstantCommons to true, it will add an entry for Commons. + * If you set $wgForeignFileRepos to an array of repostory structures, those will + * be searched after the local file repo. + * Otherwise, you will only have access to local media files. */ $wgLocalFileRepo = false; @@ -393,7 +367,7 @@ $wgUseInstantCommons = false; * Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php * * NOTE FOR WINDOWS USERS: - * To enable EXIF functions, add the folloing lines to the + * To enable EXIF functions, add the following lines to the * "Windows extensions" section of php.ini: * * extension=extensions/php_mbstring.dll @@ -402,6 +376,13 @@ $wgUseInstantCommons = false; $wgShowEXIF = function_exists( 'exif_read_data' ); /** + * If to automatically update the img_metadata field + * if the metadata field is outdated but compatible with the current version. + * Defaults to false. + */ +$wgUpdateCompatibleMetadata = false; + +/** * If you operate multiple wikis, you can define a shared upload path here. * Uploads to this wiki will NOT be put there - they will be put into * $wgUploadDirectory. @@ -434,12 +415,24 @@ $wgCacheSharedUploads = true; $wgAllowCopyUploads = false; /** * Allow asynchronous copy uploads. - * This feature is experimental. + * This feature is experimental and broken as of r81612. */ $wgAllowAsyncCopyUploads = false; /** - * Max size for uploads, in bytes. Applies to all uploads. + * Max size for uploads, in bytes. If not set to an array, applies to all + * uploads. If set to an array, per upload type maximums can be set, using the + * file and url keys. If the * key is set this value will be used as maximum + * for non-specified types. + * + * For example: + * $wgMaxUploadSize = array( + * '*' => 250 * 1024, + * 'url' => 500 * 1024, + * ); + * Sets the maximum for all uploads to 250 kB except for upload-by-url, which + * will have a maximum of 500 kB. + * */ $wgMaxUploadSize = 1024*1024*100; # 100MB @@ -536,22 +529,16 @@ $wgMimeTypeBlacklist = array( 'text/scriptlet', 'application/x-msdownload', # Windows metafile, client-side vulnerability on some systems 'application/x-msmetafile', - # A ZIP file may be a valid Java archive containing an applet which exploits the - # same-origin policy to steal cookies - 'application/zip', - - # MS Office OpenXML and other Open Package Conventions files are zip files - # and thus blacklisted just as other zip files. If you remove these entries - # from the blacklist in your local configuration, a malicious file upload - # will be able to compromise the wiki's user accounts, and the user - # accounts of any other website in the same cookie domain. - 'application/x-opc+zip', - 'application/msword', - 'application/vnd.ms-powerpoint', - 'application/vnd.msexcel', ); /** + * Allow Java archive uploads. + * This is not recommended for public wikis since a maliciously-constructed + * applet running on the same domain as the wiki can steal the user's cookies. + */ +$wgAllowJavaUploads = false; + +/** * This is a flag to determine whether or not to check file extensions on upload. * * WARNING: setting this to false is insecure for public wikis. @@ -593,7 +580,7 @@ $wgTrustedMediaFormats = array( * Each entry in the array maps a MIME type to a class name */ $wgMediaHandlers = array( - 'image/jpeg' => 'BitmapHandler', + 'image/jpeg' => 'JpegHandler', 'image/png' => 'PNGHandler', 'image/gif' => 'GIFHandler', 'image/tiff' => 'TiffHandler', @@ -645,11 +632,18 @@ $wgImageMagickTempDir = false; $wgCustomConvertCommand = false; /** + * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some image formats. + */ +$wgExiv2Command = '/usr/bin/exiv2'; + +/** * Scalable Vector Graphics (SVG) may be uploaded as images. * Since SVG support is not yet standard in browsers, it is * necessary to rasterize SVGs to PNG as a fallback format. * * An external program is required to perform this conversion. + * If set to an array, the first item is a PHP callable and any further items + * are passed as parameters after $srcPath, $dstPath, $width, $height */ $wgSVGConverters = array( 'ImageMagick' => '$path/convert -background white -thumbnail $widthx$height\! $input PNG:$output', @@ -658,6 +652,7 @@ $wgSVGConverters = array( 'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', 'rsvg' => '$path/rsvg -w$width -h$height $input $output', 'imgserv' => '$path/imgserv-wrapper -i svg -o png -w$width $input $output', + 'ImagickExt' => array( 'SvgHandler::rasterizeImagickExt' ), ); /** Pick a converter defined in $wgSVGConverters */ $wgSVGConverter = 'ImageMagick'; @@ -667,7 +662,7 @@ $wgSVGConverterPath = ''; $wgSVGMaxSize = 2048; /** Don't read SVG metadata beyond this point. * Default is 1024*256 bytes */ -$wgSVGMetadataCutoff = 262144; +$wgSVGMetadataCutoff = 262144; /** * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't @@ -744,6 +739,12 @@ $wgShowArchiveThumbnails = true; /** Obsolete, always true, kept for compatibility with extensions */ $wgUseImageResize = true; +/** + * If set to true, images that contain certain the exif orientation tag will + * be rotated accordingly. If set to null, try to auto-detect whether a scaler + * is available that can rotate. + */ +$wgEnableAutoRotation = null; /** * Internal name of virus scanner. This servers as a key to the @@ -907,7 +908,7 @@ $wgGalleryOptions = array ( 'imagesPerRow' => 0, // Default number of images per-row in the gallery. 0 -> Adapt to screensize 'imageWidth' => 120, // Width of the cells containing images in galleries (in "px") 'imageHeight' => 120, // Height of the cells containing images in galleries (in "px") - 'captionLength' => 20, // Length of caption to truncate (in characters) + 'captionLength' => 25, // Length of caption to truncate (in characters) 'showBytes' => true, // Show the filesize in bytes in categories ); @@ -976,6 +977,8 @@ $wgDjvuOutputExtension = 'jpg'; * @{ */ +$serverName = substr( $wgServer, strrpos( $wgServer, '/' ) + 1 ); + /** * Site admin email address. */ @@ -1037,6 +1040,11 @@ $wgPasswordReminderResendTime = 24; $wgNewPasswordExpiry = 3600 * 24 * 7; /** + * The time, in seconds, when an email confirmation email expires + */ +$wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60; + +/** * SMTP Mode * For using a direct (authenticated) SMTP server connection. * Default to false or fill an array : @@ -1053,6 +1061,7 @@ $wgSMTP = false; /** * Additional email parameters, will be passed as the last argument to mail() call. + * If using safe_mode this has no effect */ $wgAdditionalMailParams = null; @@ -1177,8 +1186,6 @@ $wgSQLMode = ''; /** Mediawiki schema */ $wgDBmwschema = 'mediawiki'; -/** Tsearch2 schema */ -$wgDBts2schema = 'public'; /** To override default SQLite data directory ($docroot/../data) */ $wgSQLiteDataDir = ''; @@ -1375,6 +1382,7 @@ $wgExternalServers = array(); * * $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' ); * + * @var array */ $wgDefaultExternalStore = false; @@ -1467,6 +1475,8 @@ $wgCacheDirectory = false; * - CACHE_DBA: Use PHP's DBA extension to store in a DBM-style * database. This is slow, and is not recommended for * anything other than debugging. + * - (other): A string may be used which identifies a cache + * configuration in $wgObjectCaches. * * @see $wgMessageCacheType, $wgParserCacheType */ @@ -1489,6 +1499,37 @@ $wgMessageCacheType = CACHE_ANYTHING; $wgParserCacheType = CACHE_ANYTHING; /** + * Advanced object cache configuration. + * + * Use this to define the class names and constructor parameters which are used + * for the various cache types. Custom cache types may be defined here and + * referenced from $wgMainCacheType, $wgMessageCacheType or $wgParserCacheType. + * + * The format is an associative array where the key is a cache identifier, and + * the value is an associative array of parameters. The "class" parameter is the + * class name which will be used. Alternatively, a "factory" parameter may be + * given, giving a callable function which will generate a suitable cache object. + * + * The other parameters are dependent on the class used. + */ +$wgObjectCaches = array( + CACHE_NONE => array( 'class' => 'EmptyBagOStuff' ), + CACHE_DB => array( 'class' => 'SqlBagOStuff', 'table' => 'objectcache' ), + CACHE_DBA => array( 'class' => 'DBABagOStuff' ), + + CACHE_ANYTHING => array( 'factory' => 'ObjectCache::newAnything' ), + CACHE_ACCEL => array( 'factory' => 'ObjectCache::newAccelerator' ), + CACHE_MEMCACHED => array( 'factory' => 'ObjectCache::newMemcached' ), + + 'eaccelerator' => array( 'class' => 'eAccelBagOStuff' ), + 'apc' => array( 'class' => 'APCBagOStuff' ), + 'xcache' => array( 'class' => 'XCacheBagOStuff' ), + 'wincache' => array( 'class' => 'WinCacheBagOStuff' ), + 'memcached-php' => array( 'class' => 'MemcachedPhpBagOStuff' ), + 'hash' => array( 'class' => 'HashBagOStuff' ), +); + +/** * The expiry time for the parser cache, in seconds. The default is 86.4k * seconds, otherwise known as a day. */ @@ -1512,7 +1553,7 @@ $wgSessionsInMemcached = false; * 'session_mysql.' Setting to null skips setting this entirely (which might be * useful if you're doing cross-application sessions, see bug 11381) */ -$wgSessionHandler = 'files'; +$wgSessionHandler = null; /** If enabled, will send MemCached debugging information to $wgDebugLogFile */ $wgMemCachedDebug = false; @@ -1545,6 +1586,13 @@ $wgUseLocalMessageCache = false; $wgLocalMessageCacheSerialized = true; /** + * Instead of caching everything, keep track which messages are requested and + * load only most used messages. This only makes sense if there is lots of + * interface messages customised in the wiki (like hundreds in many languages). + */ +$wgAdaptiveMessageCache = false; + +/** * Localisation cache configuration. Associative array with keys: * class: The class to use. May be overridden by extensions. * @@ -1589,7 +1637,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches do not keep obsolete copies of global * styles. */ -$wgStyleVersion = '301'; +$wgStyleVersion = '303'; /** * This will cache static pages for non-logged-in users to reduce @@ -1673,9 +1721,9 @@ $wgClockSkewFudge = 5; * to setting $wgCacheEpoch to the modification time of LocalSettings.php, as * was previously done in the default LocalSettings.php file. * - * On high-traffic wikis, this should be set to false, to avoid the need to + * On high-traffic wikis, this should be set to false, to avoid the need to * check the file modification time, and to avoid the performance impact of - * unnecessary cache invalidations. + * unnecessary cache invalidations. */ $wgInvalidateCacheOnLocalSettingsChange = true; @@ -1706,13 +1754,22 @@ $wgUseESI = false; /** Send X-Vary-Options header for better caching (requires patched Squid) */ $wgUseXVO = false; +/** Add X-Forwarded-Proto to the Vary and X-Vary-Options headers for API + * requests and RSS/Atom feeds. Use this if you have an SSL termination setup + * and need to split the cache between HTTP and HTTPS for API requests, + * feed requests and HTTP redirect responses in order to prevent cache + * pollution. This does not affect 'normal' requests to index.php other than + * HTTP redirects. + */ +$wgVaryOnXFP = false; + /** * Internal server name as known to Squid, if different. Example: * <code> * $wgInternalServer = 'http://yourinternal.tld:8000'; * </code> */ -$wgInternalServer = $wgServer; +$wgInternalServer = false; /** * Cache timeout for the squid, will be sent as s-maxage (without ESI) or @@ -1807,20 +1864,15 @@ $wgDummyLanguageCodes = array( 'als', 'bat-smg', 'be-x-old', - 'dk', 'fiu-vro', 'iu', 'nb', 'qqq', + 'qqx', + 'roa-rup', 'simple', - 'tp', ); -/** @deprecated Since MediaWiki 1.5, this must always be set to UTF-8. */ -$wgInputEncoding = 'UTF-8'; -/** @deprecated Since MediaWiki 1.5, this must always be set to UTF-8. */ -$wgOutputEncoding = 'UTF-8'; - /** * Character set for use in the article edit box. Language-specific encodings * may be defined. @@ -2060,17 +2112,7 @@ $wgLocaltimezone = null; * This setting is used for most date/time displays in the software, and is * overrideable in user preferences. It is *not* used for signature timestamps. * - * You can set it to match the configured server timezone like this: - * $wgLocalTZoffset = date("Z") / 60; - * - * If your server is not configured for the timezone you want, you can set - * this in conjunction with the signature timezone and override the PHP default - * timezone like so: - * $wgLocaltimezone="Europe/Berlin"; - * date_default_timezone_set( $wgLocaltimezone ); - * $wgLocalTZoffset = date("Z") / 60; - * - * Leave at NULL to show times in universal time (UTC/GMT). + * By default, this will be set to match $wgLocaltimezone. */ $wgLocalTZoffset = null; @@ -2084,28 +2126,49 @@ $wgLocalTZoffset = null; /** The default Content-Type header. */ $wgMimeType = 'text/html'; -/** The content type used in script tags. */ +/** + * The content type used in script tags. This is mostly going to be ignored if + * $wgHtml5 is true, at least for actual HTML output, since HTML5 doesn't + * require a MIME type for JavaScript or CSS (those are the default script and + * style languages). + */ $wgJsMimeType = 'text/javascript'; -/** The HTML document type. */ +/** + * The HTML document type. Ignored if $wgHtml5 is true, since <!DOCTYPE html> + * doesn't actually have a doctype part to put this variable's contents in. + */ $wgDocType = '-//W3C//DTD XHTML 1.0 Transitional//EN'; -/** The URL of the document type declaration. */ +/** + * The URL of the document type declaration. Ignored if $wgHtml5 is true, + * since HTML5 has no DTD, and <!DOCTYPE html> doesn't actually have a DTD part + * to put this variable's contents in. + */ $wgDTD = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'; -/** The default xmlns attribute. */ +/** + * The default xmlns attribute. Ignored if $wgHtml5 is true (or it's supposed + * to be), since we don't currently support XHTML5, and in HTML5 (i.e., served + * as text/html) the attribute has no effect, so why bother? + */ $wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml'; /** * Should we output an HTML5 doctype? If false, use XHTML 1.0 Transitional * instead, and disable HTML5 features. This may eventually be removed and set - * to always true. + * to always true. If it's true, a number of other settings will be irrelevant + * and have no effect. */ $wgHtml5 = true; /** * Defines the value of the version attribute in the <html> tag, if any. - * Will be initialized later if not set explicitly. + * This is ignored if $wgHtml5 is false. If $wgAllowRdfaAttributes and + * $wgHtml5 are both true, and this evaluates to boolean false (like if it's + * left at the default null value), it will be auto-initialized to the correct + * value for RDFa+HTML5. As such, you should have no reason to ever actually + * set this to anything. */ $wgHtml5Version = null; @@ -2145,6 +2208,9 @@ $wgWellFormedXml = true; * $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg'; * Normally we wouldn't have to define this in the root <html> * element, but IE needs it there in some circumstances. + * + * This is ignored if $wgHtml5 is true, for the same reason as + * $wgXhtmlDefaultNamespace. */ $wgXhtmlNamespaces = array(); @@ -2188,12 +2254,6 @@ $wgValidateAllHtml = false; $wgDefaultSkin = 'vector'; /** -* Should we allow the user's to select their own skin that will override the default? -* @deprecated in 1.16, use $wgHiddenPrefs[] = 'skin' to disable it -*/ -$wgAllowUserSkin = true; - -/** * Specify the name of a skin that should not be presented in the list of * available skins. Use for blacklisting a skin which you do not want to * remove from the .../skins/ directory @@ -2269,7 +2329,7 @@ $wgEnableTooltipsAndAccesskeys = true; $wgBreakFrames = false; /** - * The X-Frame-Options header to send on pages sensitive to clickjacking + * The X-Frame-Options header to send on pages sensitive to clickjacking * attacks, such as edit pages. This prevents those pages from being displayed * in a frame or iframe. The options are: * @@ -2279,9 +2339,9 @@ $wgBreakFrames = false; * to allow framing within a trusted domain. This is insecure if there * is a page on the same domain which allows framing of arbitrary URLs. * - * - false: Allow all framing. This opens up the wiki to XSS attacks and thus - * full compromise of local user accounts. Private wikis behind a - * corporate firewall are especially vulnerable. This is not + * - false: Allow all framing. This opens up the wiki to XSS attacks and thus + * full compromise of local user accounts. Private wikis behind a + * corporate firewall are especially vulnerable. This is not * recommended. * * For extra safety, set $wgBreakFrames = true, to prevent framing on all pages, @@ -2310,17 +2370,17 @@ $wgExperimentalHtmlIds = false; * You can add new icons to the built in copyright or poweredby, or you can create * a new block. Though note that you may need to add some custom css to get good styling * of new blocks in monobook. vector and modern should work without any special css. - * + * * $wgFooterIcons itself is a key/value array. - * The key is the name of a block that the icons will be wrapped in. The final id varies - * by skin; Monobook and Vector will turn poweredby into f-poweredbyico while Modern + * The key is the name of a block that the icons will be wrapped in. The final id varies + * by skin; Monobook and Vector will turn poweredby into f-poweredbyico while Modern * turns it into mw_poweredby. * The value is either key/value array of icons or a string. * In the key/value array the key may or may not be used by the skin but it can * be used to find the icon and unset it or change the icon if needed. * This is useful for disabling icons that are set by extensions. - * The value should be either a string or an array. If it is a string it will be output - * directly as html, however some skins may choose to ignore it. An array is the preferred format + * The value should be either a string or an array. If it is a string it will be output + * directly as html, however some skins may choose to ignore it. An array is the preferred format * for the icon, the following keys are used: * src: An absolute url to the image to use for the icon, this is recommended * but not required, however some skins will ignore icons without an image @@ -2346,6 +2406,13 @@ $wgFooterIcons = array( ); /** + * Login / create account link behavior when it's possible for anonymous users to create an account + * true = use a combined login / create account link + * false = split login and create account into two separate links + */ +$wgUseCombinedLoginLink = true; + +/** * Search form behavior for Vector skin only * true = use an icon search button * false = use Go & Search buttons @@ -2370,9 +2437,12 @@ $wgVectorShowVariantName = false; $wgEdititis = false; /** - * Experimental better directionality support. + * Better directionality support (bug 6100 and related). + * Removed in 1.18, still kept here for LiquidThreads backwards compatibility. + * + * @deprecated since 1.18 */ -$wgBetterDirectionality = false; +$wgBetterDirectionality = true; /** @} */ # End of output format settings } @@ -2397,6 +2467,12 @@ $wgBetterDirectionality = false; */ $wgResourceModules = array(); +/* + * Default 'remoteBasePath' value for resource loader modules. + * If not set, then $wgScriptPath will be used as a fallback. + */ +$wgResourceBasePath = null; + /** * Maximum time in seconds to cache resources served by the resource loader */ @@ -2453,6 +2529,19 @@ $wgResourceLoaderMinifierMaxLineLength = 1000; $wgIncludeLegacyJavaScript = true; /** + * Whether or not to assing configuration variables to the global window object. + * If this is set to false, old code using deprecated variables like: + * " if ( window.wgRestrictionEdit ) ..." + * or: + * " if ( wgIsArticle ) ..." + * will no longer work and needs to use mw.config instead. For example: + * " if ( mw.config.exists('wgRestrictionEdit') )" + * or + * " if ( mw.config.get('wgIsArticle') )". + */ +$wgLegacyJavaScriptGlobals = true; + +/** * If set to a positive number, ResourceLoader will not generate URLs whose * query string is more than this many characters long, and will instead use * multiple requests with shorter query strings. This degrades performance, @@ -2465,6 +2554,25 @@ $wgIncludeLegacyJavaScript = true; */ $wgResourceLoaderMaxQueryLength = -1; +/** + * If set to true, JavaScript modules loaded from wiki pages will be parsed prior + * to minification to validate it. + * + * Parse errors will result in a JS exception being thrown during module load, + * which avoids breaking other modules loaded in the same request. + */ +$wgResourceLoaderValidateJS = true; + +/** + * If set to true, statically-sourced (file-backed) JavaScript resources will + * be parsed for validity before being bundled up into ResourceLoader modules. + * + * This can be helpful for development by providing better error messages in + * default (non-debug) mode, but JavaScript parsing is slow and memory hungry + * and may fail on large pre-bundled frameworks. + */ +$wgResourceLoaderValidateStaticJS = false; + /** @} */ # End of resource loader settings } @@ -2511,6 +2619,14 @@ $wgMetaNamespaceTalk = false; $wgExtraNamespaces = array(); /** + * Same as above, but for namespaces with gender distinction. + * Note: the default form for the namespace should also be set + * using $wgExtraNamespaces for the same index. + * @since 1.18 + */ +$wgExtraGenderNamespaces = array(); + +/** * Namespace aliases * These are alternate names for the primary localised namespace names, which * are defined by $wgExtraNamespaces and the language file. If a page is @@ -2720,6 +2836,7 @@ $wgUrlProtocols = array( 'https://', 'ftp://', 'irc://', + 'ircs://', // @bug 28503 'gopher://', 'telnet://', // Well if we're going to support the above.. -ævar 'nntp://', // @bug 3808 RFC 1738 @@ -2729,6 +2846,7 @@ $wgUrlProtocols = array( 'svn://', 'git://', 'mms://', + '//', // for protocol-relative URLs ); /** @@ -2781,8 +2899,9 @@ $wgAllowImageTag = false; * - $wgTidyBin should be set to the path of the binary and * - $wgTidyConf to the path of the configuration file. * - $wgTidyOpts can include any number of parameters. - * - $wgTidyInternal controls the use of the PECL extension to use an in- - * process tidy library instead of spawning a separate program. + * - $wgTidyInternal controls the use of the PECL extension or the + * libtidy (PHP >= 5) extension to use an in-process tidy library instead + * of spawning a separate program. * Normally you shouldn't need to override the setting except for * debugging. To install, use 'pear install tidy' and add a line * 'extension=tidy.so' to php.ini. @@ -2862,6 +2981,7 @@ $wgExpensiveParserFunctionLimit = 100; /** * Preprocessor caching threshold + * Setting it to 'false' will disable the preprocessor cache. */ $wgPreprocessorCacheThreshold = 1000; @@ -2883,14 +3003,30 @@ $wgTranscludeCacheExpiry = 3600; */ /** - * Under which condition should a page in the main namespace be counted - * as a valid article? If $wgUseCommaCount is set to true, it will be - * counted if it contains at least one comma. If it is set to false - * (default), it will only be counted if it contains at least one [[wiki - * link]]. See http://www.mediawiki.org/wiki/Manual:Article_count + * Method used to determine if a page in a content namespace should be counted + * as a valid article. + * + * Redirect pages will never be counted as valid articles. * - * Retroactively changing this variable will not affect - * the existing count (cf. maintenance/recount.sql). + * This variable can have the following values: + * - 'any': all pages as considered as valid articles + * - 'comma': the page must contain a comma to be considered valid + * - 'link': the page must contain a [[wiki link]] to be considered valid + * - null: the value will be set at run time depending on $wgUseCommaCount: + * if $wgUseCommaCount is false, it will be 'link', if it is true + * it will be 'comma' + * + * See also See http://www.mediawiki.org/wiki/Manual:Article_count + * + * Retroactively changing this variable will not affect the existing count, + * to update it, you will need to run the maintenance/updateArticleCount.php + * script. + */ +$wgArticleCountMethod = null; + +/** + * Backward compatibility setting, will set $wgArticleCountMethod if it is null. + * @deprecated since 1.18; use $wgArticleCountMethod instead */ $wgUseCommaCount = false; @@ -2928,6 +3064,17 @@ $wgPasswordSalt = true; $wgMinimalPasswordLength = 1; /** + * Whether to allow password resets ("enter some identifying data, and we'll send an email + * with a temporary password you can use to get back into the account") identified by + * various bits of data. Setting all of these to false (or the whole variable to false) + * has the effect of disabling password resets entirely + */ +$wgPasswordResetRoutes = array( + 'username' => true, + 'email' => false, +); + +/** * Maximum number of Unicode characters in signature */ $wgMaxSigChars = 255; @@ -2962,8 +3109,6 @@ $wgReservedUsernames = array( $wgDefaultUserOptions = array( 'ccmeonemails' => 0, 'cols' => 80, - 'contextchars' => 50, - 'contextlines' => 5, 'date' => 'default', 'diffonly' => 0, 'disablemail' => 0, @@ -2996,7 +3141,7 @@ $wgDefaultUserOptions = array( 'numberheadings' => 0, 'previewonfirst' => 0, 'previewontop' => 1, - 'quickbar' => 1, + 'quickbar' => 5, 'rcdays' => 7, 'rclimit' => 50, 'rememberpassword' => 0, @@ -3029,7 +3174,7 @@ $wgDefaultUserOptions = array( /** * Whether or not to allow and use real name fields. - * @deprecated in 1.16, use $wgHiddenPrefs[] = 'realname' below to disable real + * @deprecated since 1.16, use $wgHiddenPrefs[] = 'realname' below to disable real * names */ $wgAllowRealName = true; @@ -3128,18 +3273,6 @@ $wgSecureLogin = false; */ /** - * Allow sysops to ban logged-in users - * @deprecated since 1.17, will be made permanently true in 1.18 - */ -$wgSysopUserBans = true; - -/** - * Allow sysops to ban IP ranges - * @deprecated since 1.17; set $wgBlockCIDRLimit to array( 'IPv4' => 32, 'IPv6 => 128 ) instead. - */ -$wgSysopRangeBans = true; - -/** * Number of seconds before autoblock entries expire. Default 86400 = 1 day. */ $wgAutoblockExpiry = 86400; @@ -3180,7 +3313,7 @@ $wgBlockDisablesLogin = false; * $wgWhitelistRead = array ( "Main Page", "Wikipedia:Help"); * </code> * - * Special:Userlogin and Special:Resetpass are always whitelisted. + * Special:Userlogin and Special:ChangePassword are always whitelisted. * * NOTE: This will only work if $wgGroupPermissions['*']['read'] is false -- * see below. Otherwise, ALL pages are accessible, regardless of this setting. @@ -3278,7 +3411,6 @@ $wgGroupPermissions['sysop']['autopatrol'] = true; $wgGroupPermissions['sysop']['protect'] = true; $wgGroupPermissions['sysop']['proxyunbannable'] = true; $wgGroupPermissions['sysop']['rollback'] = true; -$wgGroupPermissions['sysop']['trackback'] = true; $wgGroupPermissions['sysop']['upload'] = true; $wgGroupPermissions['sysop']['reupload'] = true; $wgGroupPermissions['sysop']['reupload-shared'] = true; @@ -3295,6 +3427,7 @@ $wgGroupPermissions['sysop']['movefile'] = true; $wgGroupPermissions['sysop']['unblockself'] = true; $wgGroupPermissions['sysop']['suppressredirect'] = true; #$wgGroupPermissions['sysop']['mergehistory'] = true; +#$wgGroupPermissions['sysop']['trackback'] = true; // Permission to change users' group assignments $wgGroupPermissions['bureaucrat']['userrights'] = true; @@ -3364,7 +3497,7 @@ $wgGroupsRemoveFromSelf = array(); * Set of available actions that can be restricted via action=protect * You probably shouldn't change this. * Translated through restriction-* messages. - * Title::getRestrictionTypes() will remove restrictions that are not + * Title::getRestrictionTypes() will remove restrictions that are not * applicable to a specific title (create and upload) */ $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ); @@ -3429,7 +3562,7 @@ $wgAutoConfirmCount = 0; /** * Automatically add a usergroup to any user who matches certain conditions. * The format is - * array( '&' or '|' or '^', cond1, cond2, ... ) + * array( '&' or '|' or '^' or '!', cond1, cond2, ... ) * where cond1, cond2, ... are themselves conditions; *OR* * APCOND_EMAILCONFIRMED, *OR* * array( APCOND_EMAILCONFIRMED ), *OR* @@ -3440,6 +3573,7 @@ $wgAutoConfirmCount = 0; * array( APCOND_IPINRANGE, range ), *OR* * array( APCOND_AGE_FROM_EDIT, seconds since first edit ), *OR* * array( APCOND_BLOCKED ), *OR* + * array( APCOND_ISBOT ), *OR* * similar constructs defined by extensions. * * If $wgEmailAuthentication is off, APCOND_EMAILCONFIRMED will be true for any @@ -3453,6 +3587,31 @@ $wgAutopromote = array( ); /** + * Automatically add a usergroup to any user who matches certain conditions. + * Does not add the user to the group again if it has been removed. + * Also, does not remove the group if the user no longer meets the criteria. + * + * The format is + * array( event => criteria, ... ) + * where event is + * 'onEdit' (when user edits) or 'onView' (when user views the wiki) + * and criteria has the same format as $wgAutopromote + * + * @see $wgAutopromote + * @since 1.18 + */ +$wgAutopromoteOnce = array( + 'onEdit' => array(), + 'onView' => array() +); + +/* + * Put user rights log entries for autopromotion in recent changes? + * @since 1.18 + */ +$wgAutopromoteOnceLogInRC = true; + +/** * $wgAddGroups and $wgRemoveGroups can be used to give finer control over who * can assign which groups at Special:Userrights. Example configuration: * @@ -3512,7 +3671,7 @@ $wgSummarySpamRegex = array(); * - true : block it * - false : let it through * - * @deprecated Use hooks. See SpamBlacklist extension. + * @deprecated since 1.17 Use hooks. See SpamBlacklist extension. */ $wgFilterCallback = false; @@ -3523,7 +3682,7 @@ $wgFilterCallback = false; $wgEnableDnsBlacklist = false; /** - * @deprecated Use $wgEnableDnsBlacklist instead, only kept for backward + * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for backward * compatibility */ $wgEnableSorbs = false; @@ -3535,7 +3694,7 @@ $wgEnableSorbs = false; $wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' ); /** - * @deprecated Use $wgDnsBlacklistUrls instead, only kept for backward + * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for backward * compatibility */ $wgSorbsUrl = array(); @@ -3583,17 +3742,6 @@ $wgRateLimits = array( $wgRateLimitLog = null; /** - * Array of groups which should never trigger the rate limiter - * - * @deprecated as of 1.13.0, the preferred method is using - * $wgGroupPermissions[]['noratelimit']. However, this will still - * work if desired. - * - * $wgRateLimitsExcludedGroups = array( 'sysop', 'bureaucrat' ); - */ -$wgRateLimitsExcludedGroups = array(); - -/** * Array of IPs which should be excluded from rate limits. * This may be useful for whitelisting NAT gateways for conferences, etc. */ @@ -3631,7 +3779,7 @@ $wgBlockOpenProxies = false; /** Port we want to scan for a proxy */ $wgProxyPorts = array( 80, 81, 1080, 3128, 6588, 8000, 8080, 8888, 65506 ); /** Script used to scan */ -$wgProxyScriptPath = "$IP/includes/proxy_check.php"; +$wgProxyScriptPath = "$IP/maintenance/proxy_check.php"; /** */ $wgProxyMemcExpiry = 86400; /** This should always be customised in LocalSettings.php */ @@ -3658,13 +3806,34 @@ $wgCookieExpiration = 30*86400; * or ".any.subdomain.net" */ $wgCookieDomain = ''; + + +/** + * Set this variable if you want to restrict cookies to a certain path within + * the domain specified by $wgCookieDomain. + */ $wgCookiePath = '/'; -$wgCookieSecure = ($wgProto == 'https'); + +/** + * Whether the "secure" flag should be set on the cookie. This can be: + * - true: Set secure flag + * - false: Don't set secure flag + * - "detect": Set the secure flag if $wgServer is set to an HTTPS URL + */ +$wgCookieSecure = 'detect'; + +/** + * By default, MediaWiki checks if the client supports cookies during the + * login process, so that it can display an informative error message if + * cookies are disabled. Set this to true if you want to disable this cookie + * check. + */ $wgDisableCookieCheck = false; /** - * Set $wgCookiePrefix to use a custom one. Setting to false sets the default of - * using the database name. + * Cookies generated by MediaWiki have names starting with this prefix. Set it + * to a string to use a custom prefix. Setting it to false causes the database + * name to be used as a prefix. */ $wgCookiePrefix = false; @@ -3672,10 +3841,8 @@ $wgCookiePrefix = false; * Set authentication cookies to HttpOnly to prevent access by JavaScript, * in browsers that support this feature. This can mitigates some classes of * XSS attack. - * - * Only supported on PHP 5.2 or higher. */ -$wgCookieHttpOnly = version_compare("5.2", PHP_VERSION, "<"); +$wgCookieHttpOnly = true; /** * If the requesting browser matches a regex in this blacklist, we won't @@ -3708,28 +3875,6 @@ $wgSessionName = false; * Please see math/README for more information. */ $wgUseTeX = false; -/** Location of the texvc binary */ -$wgTexvc = $IP . '/math/texvc'; -/** - * Texvc background color - * use LaTeX color format as used in \special function - * for transparent background use value 'Transparent' for alpha transparency or - * 'transparent' for binary transparency. - */ -$wgTexvcBackgroundColor = 'transparent'; - -/** - * Normally when generating math images, we double-check that the - * directories we want to write to exist, and that files that have - * been generated still exist when we need to bring them up again. - * - * This lets us give useful error messages in case of permission - * problems, and automatically rebuild images that have been lost. - * - * On a big site with heavy NFS traffic this can be slow and flaky, - * so sometimes we want to short-circuit it by setting this to false. - */ -$wgMathCheckFiles = true; /* @} */ # end LaTeX } @@ -3761,7 +3906,7 @@ $wgDebugLogPrefix = ''; $wgDebugRedirects = false; /** - * If true, log debugging data from action=raw. + * If true, log debugging data from action=raw and load.php. * This is normally false to avoid overlapping debug entries due to gen=css and * gen=js requests. */ @@ -3885,7 +4030,7 @@ $wgDebugProfiling = false; /** Output debug message on every wfProfileIn/wfProfileOut */ $wgDebugFunctionEntry = 0; -/* +/** * Destination for wfIncrStats() data... * 'cache' to go into the system cache, if enabled (memcached) * 'udp' to be sent to the UDP profiler (see $wgUDPProfilerHost) @@ -3893,6 +4038,14 @@ $wgDebugFunctionEntry = 0; */ $wgStatsMethod = 'cache'; +/** + * When $wgStatsMethod is 'udp', setting this to a string allows statistics to + * be aggregated over more than one wiki. The string will be used in place of + * the DB name in outgoing UDP packets. If this is set to false, the DB name + * will be used. + */ +$wgAggregateStatsID = false; + /** Whereas to count the number of time an article is viewed. * Does not work if pages are cached (for example with squid). */ @@ -3901,6 +4054,8 @@ $wgDisableCounters = false; /** * Support blog-style "trackbacks" for articles. See * http://www.sixapart.com/pronet/docs/trackback_spec for details. + * + * If enabling this, you also need to grant the 'trackback' right to a group */ $wgUseTrackbacks = false; @@ -3914,8 +4069,8 @@ $wgUseTrackbacks = false; * Use full paths. */ $wgParserTestFiles = array( - "$IP/maintenance/tests/parser/parserTests.txt", - "$IP/maintenance/tests/parser/ExtraParserTests.txt" + "$IP/tests/parser/parserTests.txt", + "$IP/tests/parser/extraParserTests.txt" ); /** @@ -3954,11 +4109,8 @@ $wgAdvancedSearchHighlighting = false; /** * Regexp to match word boundaries, defaults for non-CJK languages * should be empty for CJK since the words are not separate - * - * @todo FIXME: checks for lower than required PHP version (5.1.x). */ -$wgSearchHighlightBoundaries = version_compare("5.1", PHP_VERSION, "<")? '[\p{Z}\p{P}\p{C}]' - : '[ ,.;:!?~!@#$%\^&*\(\)+=\-\\|\[\]"\'<>\n\r\/{}]'; // PHP 5.0 workaround +$wgSearchHighlightBoundaries = '[\p{Z}\p{P}\p{C}]'; /** * Set to true to have the search engine count total @@ -4176,12 +4328,12 @@ $wgReadOnly = null; $wgReadOnlyFile = false; /** - * When you run the web-based upgrade utility, it will tell you what to set + * When you run the web-based upgrade utility, it will tell you what to set * this to in order to authorize the upgrade process. It will subsequently be * used as a password, to authorize further upgrades. * - * For security, do not set this to a guessable string. Use the value supplied - * by the install/upgrade process. To cause the upgrader to generate a new key, + * For security, do not set this to a guessable string. Use the value supplied + * by the install/upgrade process. To cause the upgrader to generate a new key, * delete the old key from LocalSettings.php. */ $wgUpgradeKey = false; @@ -4289,6 +4441,16 @@ $wgFeedDiffCutoff = 32768; $wgOverrideSiteFeed = array(); /** + * Available feeds objects + * Should probably only be defined when a page is syndicated ie when + * $wgOut->isSyndicated() is true + */ +$wgFeedClasses = array( + 'rss' => 'RSSFeed', + 'atom' => 'AtomFeed', +); + +/** * Which feed types should we provide by default? This can include 'rss', * 'atom', neither, or both. */ @@ -4337,16 +4499,31 @@ $wgUseTagFilter = true; * @{ */ -/** RDF metadata toggles */ -$wgEnableDublinCoreRdf = false; -$wgEnableCreativeCommonsRdf = false; - -/** Override for copyright metadata. - * TODO: these options need documentation +/** + * Override for copyright metadata. + * + * This is the name of the page containing information about the wiki's copyright status, + * which will be added as a link in the footer if it is specified. It overrides + * $wgRightsUrl if both are specified. */ $wgRightsPage = null; + +/** + * Set this to specify an external URL containing details about the content license used on your wiki. + * If $wgRightsPage is set then this setting is ignored. + */ $wgRightsUrl = null; + +/** + * If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the link. + * If using $wgRightsUrl then this value must be specified. If using $wgRightsPage then the name of the + * page will also be used as the link if this variable is not set. + */ $wgRightsText = null; + +/** + * Override for copyright metadata. + */ $wgRightsIcon = null; /** @@ -4356,17 +4533,13 @@ $wgLicenseTerms = false; /** * Set this to some HTML to override the rights icon with an arbitrary logo - * @deprecated Use $wgFooterIcons['copyright']['copyright'] + * @deprecated since 1.18 Use $wgFooterIcons['copyright']['copyright'] */ $wgCopyrightIcon = null; /** Set this to true if you want detailed copyright information forms on Upload. */ $wgUseCopyrightUpload = false; -/** Set this to false if you want to disable checking that detailed copyright - * information values are not empty. */ -$wgCheckCopyrightUpload = true; - /** * Set this to the number of authors that you want to be credited below an * article text. Set it to zero to hide the attribution block, and a negative @@ -4456,12 +4629,6 @@ $wgExportFromNamespaces = false; $wgExtensionFunctions = array(); /** - * Extension functions for initialisation of skins. This is called somewhat earlier - * than $wgExtensionFunctions. - */ -$wgSkinExtensionFunctions = array(); - -/** * Extension messages files. * * Associative array mapping extension name to the filename where messages can be @@ -4480,7 +4647,7 @@ $wgExtensionMessagesFiles = array(); /** * Aliases for special pages provided by extensions. - * @deprecated Use $specialPageAliases in a file referred to by $wgExtensionMessagesFiles + * @deprecated since 1.16 Use $specialPageAliases in a file referred to by $wgExtensionMessagesFiles */ $wgExtensionAliasesFiles = array(); @@ -4500,7 +4667,10 @@ $wgParserOutputHooks = array(); /** * List of valid skin names. - * The key should be the name in all lower case, the value should be a display name. + * The key should be the name in all lower case, the value should be a properly + * cased name for the skin. This value will be prefixed with "Skin" to create the + * class name of the skin to load, and if the skin's class cannot be found through + * the autoloader it will be used to load a .php file by that name in the skins directory. * The default skins will be added later, by Skin::getSkinNames(). Use * Skin::getSkinNames() as an accessor if you wish to have access to the full list. */ @@ -4542,6 +4712,7 @@ $wgExtensionCredits = array(); /** * Authentication plugin. + * @var AuthPlugin */ $wgAuth = null; @@ -4572,6 +4743,17 @@ $wgJobClasses = array( ); /** + * Jobs that must be explicitly requested, i.e. aren't run by job runners unless special flags are set. + * + * These can be: + * - Very long-running jobs. + * - Jobs that you would never want to run as part of a page rendering request. + * - Jobs that you want to run on specialized machines ( like transcoding, or a particular + * machine on your cluster has 'outside' web access you could restrict uploadFromUrl ) + */ +$wgJobTypesExcludedFromDefaultQueue = array(); + +/** * Additional functions to be performed with updateSpecialPages. * Expensive Querypages are already updated. */ @@ -4624,24 +4806,29 @@ $wgCategoryMagicGallery = true; $wgCategoryPagingLimit = 200; /** - * Specify how category names should be sorted, when listed on a category page. + * Specify how category names should be sorted, when listed on a category page. * A sorting scheme is also known as a collation. * * Available values are: * * - uppercase: Converts the category name to upper case, and sorts by that. * - * - uca-default: Provides access to the Unicode Collation Algorithm with + * - identity: Does no conversion. Sorts by binary value of the string. + * + * - uca-default: Provides access to the Unicode Collation Algorithm with * the default element table. This is a compromise collation which sorts * all languages in a mediocre way. However, it is better than "uppercase". * - * To use the uca-default collation, you must have PHP's intl extension - * installed. See http://php.net/manual/en/intl.setup.php . The details of the - * resulting collation will depend on the version of ICU installed on the + * To use the uca-default collation, you must have PHP's intl extension + * installed. See http://php.net/manual/en/intl.setup.php . The details of the + * resulting collation will depend on the version of ICU installed on the * server. * * After you change this, you must run maintenance/updateCollation.php to fix - * the sort keys in the database. + * the sort keys in the database. + * + * Extensions can define there own collations by subclassing Collation + * and using the Collation::factory hook. */ $wgCategoryCollation = 'uppercase'; @@ -4753,34 +4940,34 @@ $wgLogHeaders = array( * Extensions with custom log types may add to this array. */ $wgLogActions = array( - 'block/block' => 'blocklogentry', - 'block/unblock' => 'unblocklogentry', - 'block/reblock' => 'reblock-logentry', - 'protect/protect' => 'protectedarticle', - 'protect/modify' => 'modifiedarticleprotection', - 'protect/unprotect' => 'unprotectedarticle', - 'protect/move_prot' => 'movedarticleprotection', - 'rights/rights' => 'rightslogentry', - 'rights/disable' => 'disableaccount-logentry', - 'delete/delete' => 'deletedarticle', - 'delete/restore' => 'undeletedarticle', - 'delete/revision' => 'revdelete-logentry', - 'delete/event' => 'logdelete-logentry', - 'upload/upload' => 'uploadedimage', - 'upload/overwrite' => 'overwroteimage', - 'upload/revert' => 'uploadedimage', - 'move/move' => '1movedto2', - 'move/move_redir' => '1movedto2_redir', - 'import/upload' => 'import-logentry-upload', - 'import/interwiki' => 'import-logentry-interwiki', - 'merge/merge' => 'pagemerge-logentry', - 'suppress/revision' => 'revdelete-logentry', - 'suppress/file' => 'revdelete-logentry', - 'suppress/event' => 'logdelete-logentry', - 'suppress/delete' => 'suppressedarticle', - 'suppress/block' => 'blocklogentry', - 'suppress/reblock' => 'reblock-logentry', - 'patrol/patrol' => 'patrol-log-line', + 'block/block' => 'blocklogentry', + 'block/unblock' => 'unblocklogentry', + 'block/reblock' => 'reblock-logentry', + 'protect/protect' => 'protectedarticle', + 'protect/modify' => 'modifiedarticleprotection', + 'protect/unprotect' => 'unprotectedarticle', + 'protect/move_prot' => 'movedarticleprotection', + 'rights/rights' => 'rightslogentry', + 'rights/autopromote' => 'rightslogentry-autopromote', + 'delete/delete' => 'deletedarticle', + 'delete/restore' => 'undeletedarticle', + 'delete/revision' => 'revdelete-logentry', + 'delete/event' => 'logdelete-logentry', + 'upload/upload' => 'uploadedimage', + 'upload/overwrite' => 'overwroteimage', + 'upload/revert' => 'uploadedimage', + 'move/move' => '1movedto2', + 'move/move_redir' => '1movedto2_redir', + 'import/upload' => 'import-logentry-upload', + 'import/interwiki' => 'import-logentry-interwiki', + 'merge/merge' => 'pagemerge-logentry', + 'suppress/revision' => 'revdelete-logentry', + 'suppress/file' => 'revdelete-logentry', + 'suppress/event' => 'logdelete-logentry', + 'suppress/delete' => 'suppressedarticle', + 'suppress/block' => 'blocklogentry', + 'suppress/reblock' => 'reblock-logentry', + 'patrol/patrol' => 'patrol-log-line', ); /** @@ -4795,11 +4982,6 @@ $wgLogActionsHandlers = array(); */ $wgNewUserLog = true; -/** - * Log the automatic creations of new users accounts? - */ -$wgLogAutocreatedAccounts = false; - /** @} */ # end logging } /*************************************************************************//** @@ -4868,16 +5050,18 @@ $wgSpecialPageGroups = array( 'Listusers' => 'users', 'Activeusers' => 'users', 'Listgrouprights' => 'users', - 'Ipblocklist' => 'users', + 'BlockList' => 'users', 'Contributions' => 'users', 'Emailuser' => 'users', 'Listadmins' => 'users', 'Listbots' => 'users', 'Userrights' => 'users', - 'Blockip' => 'users', + 'Block' => 'users', + 'Unblock' => 'users', 'Preferences' => 'users', - 'Resetpass' => 'users', + 'ChangePassword' => 'users', 'DeletedContributions' => 'users', + 'PasswordReset' => 'users', 'Mostlinked' => 'highuse', 'Mostlinkedcategories' => 'highuse', @@ -4926,12 +5110,6 @@ $wgSpecialPageGroups = array( $wgSortSpecialPages = true; /** - * Filter for Special:Randompage. Part of a WHERE clause - * @deprecated as of 1.16, use the SpecialRandomGetRandomTitle hook - */ -$wgExtraRandompageSQL = false; - -/** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. */ @@ -4946,6 +5124,47 @@ $wgMaxRedirectLinksRetrieved = 500; /** @} */ # end special pages } /*************************************************************************//** + * @name Actions + * @{ + */ + +/** + * Array of allowed values for the title=foo&action=<action> parameter. Syntax is: + * 'foo' => 'ClassName' Load the specified class which subclasses Action + * 'foo' => true Load the class FooAction which subclasses Action + * If something is specified in the getActionOverrides() + * of the relevant Page object it will be used + * instead of the default class. + * 'foo' => false The action is disabled; show an error message + * Unsetting core actions will probably cause things to complain loudly. + */ +$wgActions = array( + 'credits' => true, + 'deletetrackback' => true, + 'info' => true, + 'markpatrolled' => true, + 'purge' => true, + 'revert' => true, + 'revisiondelete' => true, + 'rollback' => true, + 'unwatch' => true, + 'watch' => true, +); + +/** + * Array of disabled article actions, e.g. view, edit, delete, etc. + * @deprecated since 1.18; just set $wgActions['action'] = false instead + */ +$wgDisabledActions = array(); + +/** + * Allow the "info" action, very inefficient at the moment + */ +$wgAllowPageInfo = false; + +/** @} */ # end actions } + +/*************************************************************************//** * @name Robot (search engine crawler) policy * See also $wgNoFollowLinks. * @{ @@ -5060,14 +5279,7 @@ $wgAPIMaxUncachedDiffs = 1; $wgAPIRequestLog = false; /** - * Cache the API help text for up to an hour. Disable this during API - * debugging and development - */ -$wgAPICacheHelp = true; - -/** - * Set the timeout for the API help text cache. Ignored if $wgAPICacheHelp - * is false. + * Set the timeout for the API help text cache. If set to 0, caching disabled */ $wgAPICacheHelpTimeout = 60*60; @@ -5080,12 +5292,11 @@ $wgUseAjax = true; * List of Ajax-callable functions. * Extensions acting as Ajax callbacks must register here */ -$wgAjaxExportList = array( 'wfAjaxGetFileUrl' ); +$wgAjaxExportList = array(); /** * Enable watching/unwatching pages using AJAX. * Requires $wgUseAjax to be true too. - * Causes wfAjaxWatch to be added to $wgAjaxExportList */ $wgAjaxWatch = true; @@ -5214,20 +5425,62 @@ $wgUpdateRowsPerQuery = 100; /** @} */ # End job queue } /************************************************************************//** - * @name Miscellaneous + * @name HipHop compilation * @{ */ -/** Allow the "info" action, very inefficient at the moment */ -$wgAllowPageInfo = false; +/** + * The build directory for HipHop compilation. + * Defaults to $IP/maintenance/hiphop/build. + */ +$wgHipHopBuildDirectory = false; -/** Name of the external diff engine to use */ -$wgExternalDiffEngine = false; +/** + * The HipHop build type. Can be either "Debug" or "Release". + */ +$wgHipHopBuildType = 'Debug'; /** - * Array of disabled article actions, e.g. view, edit, dublincore, delete, etc. + * Number of parallel processes to use during HipHop compilation, or "detect" + * to guess from system properties. */ -$wgDisabledActions = array(); +$wgHipHopCompilerProcs = 'detect'; + +/** + * Filesystem extensions directory. Defaults to $IP/../extensions. + * + * To compile extensions with HipHop, set $wgExtensionsDirectory correctly, + * and use code like: + * + * require( MWInit::extensionSetupPath( 'Extension/Extension.php' ) ); + * + * to include the extension setup file from LocalSettings.php. It is not + * necessary to set this variable unless you use MWInit::extensionSetupPath(). + */ +$wgExtensionsDirectory = false; + +/** + * A list of files that should be compiled into a HipHop build, in addition to + * those listed in $wgAutoloadClasses. Add to this array in an extension setup + * file in order to add files to the build. + * + * The files listed here must either be either absolute paths under $IP or + * under $wgExtensionsDirectory, or paths relative to the virtual source root + * "$IP/..", i.e. starting with "phase3" for core files, and "extensions" for + * extension files. + */ +$wgCompiledFiles = array(); + +/** @} */ # End of HipHop compilation } + + +/************************************************************************//** + * @name Miscellaneous + * @{ + */ + +/** Name of the external diff engine to use */ +$wgExternalDiffEngine = false; /** * Disable redirects to special pages and interwiki redirects, which use a 302 @@ -5296,6 +5549,8 @@ $wgUploadMaintenance = false; $wgEnableSelenium = false; $wgSeleniumTestConfigs = array(); $wgSeleniumConfigFile = null; +$wgDBtestuser = ''; //db user that has permission to create and drop the test databases only +$wgDBtestpassword = ''; /** * For really cool vim folding this needs to be at the end: diff --git a/includes/Defines.php b/includes/Defines.php index 64197d9c..ff7d7980 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -1,6 +1,11 @@ <?php /** - * A few constants that might be needed during LocalSettings.php + * A few constants that might be needed during LocalSettings.php. + * + * Note: these constants must all be resolvable at compile time by HipHop, + * since this file will not be executed during request startup for a compiled + * MediaWiki. + * * @file */ @@ -80,27 +85,6 @@ define( 'NS_IMAGE', NS_FILE ); define( 'NS_IMAGE_TALK', NS_FILE_TALK ); /**@}*/ -/** - * Available feeds objects - * Should probably only be defined when a page is syndicated ie when - * $wgOut->isSyndicated() is true - */ -$wgFeedClasses = array( - 'rss' => 'RSSFeed', - 'atom' => 'AtomFeed', -); - -/**@{ - * Maths constants - */ -define( 'MW_MATH_PNG', 0 ); -define( 'MW_MATH_SIMPLE', 1 ); -define( 'MW_MATH_HTML', 2 ); -define( 'MW_MATH_SOURCE', 3 ); -define( 'MW_MATH_MODERN', 4 ); -define( 'MW_MATH_MATHML', 5 ); -/**@}*/ - /**@{ * Cache type */ @@ -114,7 +98,7 @@ define( 'CACHE_DBA', 4 ); // Use PHP's DBA extension to store in a DBM-st /**@{ * Media types. - * This defines constants for the value returned by Image::getMediaType() + * This defines constants for the value returned by File::getMediaType() */ define( 'MEDIATYPE_UNKNOWN', 'UNKNOWN' ); // unknown format define( 'MEDIATYPE_BITMAP', 'BITMAP' ); // some bitmap image or image source (like psd, etc). Can't scale up. @@ -254,4 +238,15 @@ define( 'APCOND_ISIP', 5 ); define( 'APCOND_IPINRANGE', 6 ); define( 'APCOND_AGE_FROM_EDIT', 7 ); define( 'APCOND_BLOCKED', 8 ); +define( 'APCOND_ISBOT', 9 ); /**@}*/ + +/** + * Protocol constants for wfExpandUrl() + */ +define( 'PROTO_HTTP', 'http://' ); +define( 'PROTO_HTTPS', 'https://' ); +define( 'PROTO_RELATIVE', '//' ); +define( 'PROTO_CURRENT', null ); +define( 'PROTO_CANONICAL', 1 ); +define( 'PROTO_INTERNAL', 2 ); diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php index cccb070a..80b7408c 100644 --- a/includes/DjVuImage.php +++ b/includes/DjVuImage.php @@ -72,7 +72,7 @@ class DjVuImage { function dump() { $file = fopen( $this->mFilename, 'rb' ); $header = fread( $file, 12 ); - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4magic/a4chunk/NchunkLength', $header ) ); echo "$chunk $chunkLength\n"; $this->dumpForm( $file, $chunkLength, 1 ); @@ -88,7 +88,7 @@ class DjVuImage { if( $chunkHeader == '' ) { break; } - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) ); echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n"; @@ -119,7 +119,7 @@ class DjVuImage { if( strlen( $header ) < 16 ) { wfDebug( __METHOD__ . ": too short file header\n" ); } else { - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) ); if( $magic != 'AT&T' ) { @@ -143,7 +143,7 @@ class DjVuImage { if( strlen( $header ) < 8 ) { return array( false, 0 ); } else { - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4chunk/Nlength', $header ) ); return array( $chunk, $length ); } @@ -202,7 +202,7 @@ class DjVuImage { return false; } - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'nwidth/' . 'nheight/' . @@ -269,7 +269,7 @@ class DjVuImage { EOR; $txt = preg_replace_callback( $reg, array( $this, 'pageTextCallback' ), $txt ); $txt = "<DjVuTxt>\n<HEAD></HEAD>\n<BODY>\n" . $txt . "</BODY>\n</DjVuTxt>\n"; - $xml = preg_replace( "/<DjVuXML>/", "<mw-djvu><DjVuXML>", $xml ); + $xml = preg_replace( "/<DjVuXML>/", "<mw-djvu><DjVuXML>", $xml, 1 ); $xml = $xml . $txt. '</mw-djvu>' ; } } diff --git a/includes/EditPage.php b/includes/EditPage.php index 3e85ad10..e6e7111d 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -11,7 +11,7 @@ * interfaces. * * EditPage cares about two distinct titles: - * $wgTitle is the page that forms submit to, links point to, + * $this->mContextTitle is the page that forms submit to, links point to, * redirects go to, etc. $this->mTitle (as well as $mArticle) is the * page in the database that is actually being edited. These are * usually the same, but they are now allowed to be different. @@ -42,20 +42,31 @@ class EditPage { const AS_IMAGE_REDIRECT_ANON = 233; const AS_IMAGE_REDIRECT_LOGGED = 234; + /** + * @var Article + */ var $mArticle; + + /** + * @var Title + */ var $mTitle; + private $mContextTitle = null; var $action; var $isConflict = false; var $isCssJsSubpage = false; var $isCssSubpage = false; var $isJsSubpage = false; - var $deletedSinceEdit = false; + var $isWrongCaseCssJsPage = false; + var $isNew = false; // new page or new section + var $deletedSinceEdit; var $formtype; var $firsttime; var $lastDelete; var $mTokenOk = false; var $mTokenOkExceptSuffix = false; var $mTriedSave = false; + var $incompleteForm = false; var $tooBig = false; var $kblength = false; var $missingComment = false; @@ -64,7 +75,12 @@ class EditPage { var $autoSumm = ''; var $hookError = ''; #var $mPreviewTemplates; + + /** + * @var ParserOutput + */ var $mParserOutput; + var $mBaseRevision = false; var $mShowSummaryField = true; @@ -85,6 +101,7 @@ class EditPage { public $editFormTextBottom; public $editFormTextAfterContent; public $previewTextAfterContent; + public $mPreloadText; /* $didSave should be set to true whenever an article was succesfully altered. */ public $didSave = false; @@ -94,7 +111,7 @@ class EditPage { /** * @todo document - * @param $article + * @param $article Article */ function __construct( $article ) { $this->mArticle =& $article; @@ -113,18 +130,47 @@ class EditPage { $this->mPreloadText = ""; } + /** + * @return Article + */ function getArticle() { return $this->mArticle; } + /** + * Set the context Title object + * + * @param $title Title object or null + */ + public function setContextTitle( $title ) { + $this->mContextTitle = $title; + } + + /** + * Get the context title object. + * If not set, $wgTitle will be returned. This behavior might changed in + * the future to return $this->mTitle instead. + * + * @return Title object + */ + public function getContextTitle() { + if ( is_null( $this->mContextTitle ) ) { + global $wgTitle; + return $wgTitle; + } else { + return $this->mContextTitle; + } + } /** * Fetch initial editing page content. + * + * @param $def_text string * @returns mixed string on success, $def_text for invalid sections * @private */ function getContent( $def_text = '' ) { - global $wgOut, $wgRequest, $wgParser, $wgContLang, $wgMessageCache; + global $wgOut, $wgRequest, $wgParser; wfProfileIn( __METHOD__ ); # Get variables from query string :P @@ -141,10 +187,10 @@ class EditPage { if ( !$this->mTitle->exists() ) { if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { # If this is a system message, get the default text. - list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) ); - $text = wfMsgGetKey( $message, false, $lang, false ); - if( wfEmptyMsg( $message, $text ) ) + $text = $this->mTitle->getDefaultMessageText(); + if( $text === false ) { $text = $this->getPreloadedText( $preload ); + } } else { # If requested, preload some text. $text = $this->getPreloadedText( $preload ); @@ -198,7 +244,7 @@ class EditPage { // was created, or we may simply have got bogus input. $this->editFormPageTop .= $wgOut->parse( '<div class="error mw-undo-norev">' . wfMsgNoTrans( 'undo-norev' ) . '</div>' ); } - } else if ( $section != '' ) { + } elseif ( $section != '' ) { if ( $section == 'new' ) { $text = $this->getPreloadedText( $preload ); } else { @@ -212,7 +258,11 @@ class EditPage { return $text; } - /** Use this method before edit() to preload some text into the edit box */ + /** + * Use this method before edit() to preload some text into the edit box + * + * @param $text string + */ public function setPreloadedText( $text ) { $this->mPreloadText = $text; } @@ -253,15 +303,19 @@ class EditPage { return ''; } - /* + /** * Check if a page was deleted while the user was editing it, before submit. * Note that we rely on the logging table, which hasn't been always there, * but that doesn't matter, because this only applies to brand new * deletes. */ protected function wasDeletedSinceLastEdit() { - if ( $this->deletedSinceEdit ) - return true; + if ( $this->deletedSinceEdit !== null ) { + return $this->deletedSinceEdit; + } + + $this->deletedSinceEdit = false; + if ( $this->mTitle->isDeletedQuick() ) { $this->lastDelete = $this->getLastDelete(); if ( $this->lastDelete ) { @@ -271,12 +325,15 @@ class EditPage { } } } + return $this->deletedSinceEdit; } /** * Checks whether the user entered a skin name in uppercase, * e.g. "User:Example/Monobook.css" instead of "monobook.css" + * + * @return bool */ protected function isWrongCaseCssJsPage() { if( $this->mTitle->isCssJsSubpage() ) { @@ -335,7 +392,7 @@ class EditPage { $this->preview = true; } - $wgOut->addModules( array( 'mediawiki.legacy.edit', 'mediawiki.action.edit' ) ); + $wgOut->addModules( array( 'mediawiki.action.edit' ) ); if ( $wgUser->getOption( 'uselivepreview', false ) ) { $wgOut->addModules( 'mediawiki.legacy.preview' ); @@ -345,6 +402,9 @@ class EditPage { $permErrors = $this->getEditPermissionErrors(); if ( $permErrors ) { + // Auto-block user's IP if the account was "hard" blocked + $wgUser->spreadAnyEditBlock(); + wfDebug( __METHOD__ . ": User can't edit\n" ); $content = $this->getContent( null ); $content = $content === '' ? null : $content; @@ -354,9 +414,9 @@ class EditPage { } else { if ( $this->save ) { $this->formtype = 'save'; - } else if ( $this->preview ) { + } elseif ( $this->preview ) { $this->formtype = 'preview'; - } else if ( $this->diff ) { + } elseif ( $this->diff ) { $this->formtype = 'diff'; } else { # First time through $this->firsttime = true; @@ -377,10 +437,11 @@ class EditPage { $this->isConflict = false; // css / js subpages of user pages get a special treatment - $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage(); - $this->isCssSubpage = $this->mTitle->isCssSubpage(); - $this->isJsSubpage = $this->mTitle->isJsSubpage(); + $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage(); + $this->isCssSubpage = $this->mTitle->isCssSubpage(); + $this->isJsSubpage = $this->mTitle->isJsSubpage(); $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage(); + $this->isNew = !$this->mTitle->exists() || $this->section == 'new'; # Show applicable editing introductions if ( $this->formtype == 'initial' || $this->firsttime ) @@ -392,16 +453,18 @@ class EditPage { # Optional notices on a per-namespace and per-page basis $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace(); - if ( !wfEmptyMsg( $editnotice_ns, wfMsgForContent( $editnotice_ns ) ) ) { - $wgOut->addWikiText( wfMsgForContent( $editnotice_ns ) ); + $editnotice_ns_message = wfMessage( $editnotice_ns )->inContentLanguage(); + if ( $editnotice_ns_message->exists() ) { + $wgOut->addWikiText( $editnotice_ns_message->plain() ); } if ( MWNamespace::hasSubpages( $this->mTitle->getNamespace() ) ) { $parts = explode( '/', $this->mTitle->getDBkey() ); $editnotice_base = $editnotice_ns; while ( count( $parts ) > 0 ) { $editnotice_base .= '-'.array_shift( $parts ); - if ( !wfEmptyMsg( $editnotice_base, wfMsgForContent( $editnotice_base ) ) ) { - $wgOut->addWikiText( wfMsgForContent( $editnotice_base ) ); + $editnotice_base_msg = wfMessage( $editnotice_base )->inContentLanguage(); + if ( $editnotice_base_msg->exists() ) { + $wgOut->addWikiText( $editnotice_base_msg->plain() ); } } } @@ -439,6 +502,9 @@ class EditPage { wfProfileOut( __METHOD__ ); } + /** + * @return array + */ protected function getEditPermissionErrors() { global $wgUser; $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ); @@ -532,7 +598,7 @@ class EditPage { /** * @todo document - * @param $request + * @param $request WebRequest */ function importFormData( &$request ) { global $wgLang, $wgUser; @@ -559,7 +625,7 @@ class EditPage { } # Truncate for whole multibyte characters. +5 bytes for ellipsis - $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250, '' ); + $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); # Remove extra headings from summaries and new sections. $this->summary = preg_replace('/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary); @@ -569,7 +635,17 @@ class EditPage { $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' ); - if ( is_null( $this->edittime ) ) { + if ($this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null) { + // wpTextbox1 field is missing, possibly due to being "too big" + // according to some filter rules such as Suhosin's setting for + // suhosin.request.max_value_length (d'oh) + $this->incompleteForm = true; + } else { + // edittime should be one of our last fields; if it's missing, + // the submission probably broke somewhere in the middle. + $this->incompleteForm = is_null( $this->edittime ); + } + if ( $this->incompleteForm ) { # If the form is incomplete, force to preview. wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" ); wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" ); @@ -589,7 +665,7 @@ class EditPage { # The unmarked state will be assumed to be a save, # if the form seems otherwise complete. wfDebug( __METHOD__ . ": Passed token check.\n" ); - } else if ( $this->diff ) { + } elseif ( $this->diff ) { # Failed token check, but only requested "Show Changes". wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" ); } else { @@ -653,7 +729,7 @@ class EditPage { $this->bot = $request->getBool( 'bot', true ); $this->nosummary = $request->getBool( 'nosummary' ); - // FIXME: unused variable? + // @todo FIXME: Unused variable? $this->oldid = $request->getInt( 'oldid' ); $this->live = $request->getCheck( 'live' ); @@ -719,8 +795,8 @@ class EditPage { $ip = User::isIP( $username ); if ( !$user->isLoggedIn() && !$ip ) { # User does not exist $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>", - array( 'userpage-userdoesnotexist', $username ) ); - } else if ( $user->isBlocked() ) { # Show log extract if the user is currently blocked + array( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ) ); + } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked LogEventsList::showLogExtract( $wgOut, 'block', @@ -766,8 +842,8 @@ class EditPage { $title = Title::newFromText( $this->editintro ); if ( $title instanceof Title && $title->exists() && $title->userCanRead() ) { global $wgOut; - $revision = Revision::newFromTitle( $title ); - $wgOut->addWikiTextTitleTidy( $revision->getText(), $this->mTitle ); + // Added using template syntax, to take <noinclude>'s into account. + $wgOut->addWikiTextTitleTidy( '{{:' . $title->getFullText() . '}}', $this->mTitle ); return true; } else { return false; @@ -779,32 +855,46 @@ class EditPage { /** * Attempt submission (no UI) - * @return one of the constants describing the result + * + * @param $result + * @param $bot bool + * + * @return Status object, possibly with a message, but always with one of the AS_* constants in $status->value, + * + * FIXME: This interface is TERRIBLE, but hard to get rid of due to various error display idiosyncrasies. There are + * also lots of cases where error metadata is set in the object and retrieved later instead of being returned, e.g. + * AS_CONTENT_TOO_BIG and AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some time. */ function internalAttemptSave( &$result, $bot = false ) { global $wgFilterCallback, $wgUser, $wgParser; global $wgMaxArticleSize; + + $status = Status::newGood(); wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__ . '-checks' ); if ( !wfRunHooks( 'EditPage::attemptSave', array( $this ) ) ) { wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR; + return $status; } # Check image redirect if ( $this->mTitle->getNamespace() == NS_FILE && Title::newFromRedirect( $this->textbox1 ) instanceof Title && !$wgUser->isAllowed( 'upload' ) ) { - $isAnon = $wgUser->isAnon(); + $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; + $status->setResult( false, $code ); wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); - return $isAnon ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; + return $status; } # Check for spam @@ -818,276 +908,350 @@ class EditPage { $pdbk = $this->mTitle->getPrefixedDBkey(); $match = str_replace( "\n", '', $match ); wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); + $status->fatal( 'spamprotectionmatch', $match ); + $status->value = self::AS_SPAM_ERROR; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_SPAM_ERROR; + return $status; } if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) { # Error messages or other handling should be performed by the filter function + $status->setResult( false, self::AS_FILTERING ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_FILTERING; + return $status; } if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR; + return $status; } elseif ( $this->hookError != '' ) { # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR_EXPECTED; + return $status; } if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { + // Auto-block user's IP if the account was "hard" blocked + $wgUser->spreadAnyEditBlock(); # Check block state against master, thus 'false'. + $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_BLOCKED_PAGE_FOR_USER; + return $status; } $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 ); if ( $this->kblength > $wgMaxArticleSize ) { // Error will be displayed by showEditForm() $this->tooBig = true; + $status->setResult( false, self::AS_CONTENT_TOO_BIG ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_CONTENT_TOO_BIG; + return $status; } if ( !$wgUser->isAllowed( 'edit' ) ) { if ( $wgUser->isAnon() ) { + $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_READ_ONLY_PAGE_ANON; + return $status; } else { + $status->fatal( 'readonlytext' ); + $status->value = self::AS_READ_ONLY_PAGE_LOGGED; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_READ_ONLY_PAGE_LOGGED; + return $status; } } if ( wfReadOnly() ) { + $status->fatal( 'readonlytext' ); + $status->value = self::AS_READ_ONLY_PAGE; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_READ_ONLY_PAGE; + return $status; } if ( $wgUser->pingLimiter() ) { + $status->fatal( 'actionthrottledtext' ); + $status->value = self::AS_RATE_LIMITED; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_RATE_LIMITED; + return $status; } # If the article has been deleted while editing, don't save it without # confirmation if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) { + $status->setResult( false, self::AS_ARTICLE_WAS_DELETED ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_ARTICLE_WAS_DELETED; + return $status; } wfProfileOut( __METHOD__ . '-checks' ); # If article is new, insert it. $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE ); - if ( 0 == $aid ) { + $new = ( $aid == 0 ); + + if ( $new ) { // Late check for create permission, just in case *PARANOIA* if ( !$this->mTitle->userCan( 'create' ) ) { + $status->fatal( 'nocreatetext' ); + $status->value = self::AS_NO_CREATE_PERMISSION; wfDebug( __METHOD__ . ": no create permission\n" ); wfProfileOut( __METHOD__ ); - return self::AS_NO_CREATE_PERMISSION; + return $status; } # Don't save a new article if it's blank. if ( $this->textbox1 == '' ) { + $status->setResult( false, self::AS_BLANK_ARTICLE ); wfProfileOut( __METHOD__ ); - return self::AS_BLANK_ARTICLE; + return $status; } // Run post-section-merge edit filter if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR; + return $status; } elseif ( $this->hookError != '' ) { # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR_EXPECTED; + return $status; } # Handle the user preference to force summaries here. Check if it's not a redirect. if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) { if ( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; + $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh + $status->value = self::AS_SUMMARY_NEEDED; wfProfileOut( __METHOD__ ); - return self::AS_SUMMARY_NEEDED; + return $status; } } - $isComment = ( $this->section == 'new' ); + $text = $this->textbox1; + if ( $this->section == 'new' && $this->summary != '' ) { + $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text; + } - $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, - $this->minoredit, $this->watchthis, false, $isComment, $bot ); + $status->value = self::AS_SUCCESS_NEW_ARTICLE; - wfProfileOut( __METHOD__ ); - return self::AS_SUCCESS_NEW_ARTICLE; - } + } else { - # Article exists. Check for edit conflict. + # Article exists. Check for edit conflict. - $this->mArticle->clear(); # Force reload of dates, etc. - $this->mArticle->forUpdate( true ); # Lock the article + $this->mArticle->clear(); # Force reload of dates, etc. - wfDebug( "timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n" ); + wfDebug( "timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n" ); - if ( $this->mArticle->getTimestamp() != $this->edittime ) { - $this->isConflict = true; - if ( $this->section == 'new' ) { - if ( $this->mArticle->getUserText() == $wgUser->getName() && - $this->mArticle->getComment() == $this->summary ) { - // Probably a duplicate submission of a new comment. - // This can happen when squid resends a request after - // a timeout but the first one actually went through. - wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); - } else { - // New comment; suppress conflict. - $this->isConflict = false; - wfDebug( __METHOD__ .": conflict suppressed; new section\n" ); + if ( $this->mArticle->getTimestamp() != $this->edittime ) { + $this->isConflict = true; + if ( $this->section == 'new' ) { + if ( $this->mArticle->getUserText() == $wgUser->getName() && + $this->mArticle->getComment() == $this->summary ) { + // Probably a duplicate submission of a new comment. + // This can happen when squid resends a request after + // a timeout but the first one actually went through. + wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); + } else { + // New comment; suppress conflict. + $this->isConflict = false; + wfDebug( __METHOD__ .": conflict suppressed; new section\n" ); + } } } - } - $userid = $wgUser->getId(); - - # Suppress edit conflict with self, except for section edits where merging is required. - if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit( $userid, $this->edittime ) ) { - wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); - $this->isConflict = false; - } + $userid = $wgUser->getId(); - if ( $this->isConflict ) { - wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '" . - $this->mArticle->getTimestamp() . "')\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); - } else { - wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary ); - } - if ( is_null( $text ) ) { - wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); |